@axlsdk/axl 0.22.1 → 0.22.3

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/dist/index.js CHANGED
@@ -1205,7 +1205,8 @@ var OpenAICompatibleProvider = class {
1205
1205
  return this.name === "openrouter" && model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64"] } } : {};
1206
1206
  }
1207
1207
  validateInput(request) {
1208
- const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
1208
+ const modelOverride = request.providerOptions?.model;
1209
+ const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
1209
1210
  const fail = (source, feature) => {
1210
1211
  throw new UnsupportedModelInputError({
1211
1212
  provider: this.name,
@@ -1215,6 +1216,9 @@ var OpenAICompatibleProvider = class {
1215
1216
  ...feature ? { feature } : {}
1216
1217
  });
1217
1218
  };
1219
+ if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
1220
+ fail(void 0, "invalid model providerOptions");
1221
+ }
1218
1222
  if (this.name !== "openrouter" || effectiveModel.trim().length === 0) {
1219
1223
  fail(void 0, "image input for this model");
1220
1224
  }
@@ -2114,17 +2118,22 @@ function resolveOpenAIReasoningEffort(model, resolved) {
2114
2118
  }
2115
2119
  return resolved.thinkingDisabled ? clampReasoningEffort(model, "none") : void 0;
2116
2120
  }
2117
- var warnedChatMaxDowngrades = /* @__PURE__ */ new Set();
2118
2121
  function resolveOpenAIChatReasoningEffort(model, resolved) {
2119
2122
  const effort = resolveOpenAIReasoningEffort(model, resolved);
2120
- if (effort !== "max") return effort;
2121
- if (resolved.activeEffort === "max" && !warnedChatMaxDowngrades.has(model)) {
2122
- warnedChatMaxDowngrades.add(model);
2123
- console.warn(
2124
- `[axl] OpenAI Chat Completions does not accept effort "max" for ${model}; using "xhigh". Use the OpenAI Responses provider for native "max".`
2125
- );
2126
- }
2127
- return "xhigh";
2123
+ return effort === "max" ? "xhigh" : effort;
2124
+ }
2125
+ function resolveOpenAIEffortResolution(options, resolveWireEffort, endpoint) {
2126
+ const resolved = resolveThinkingOptions(options);
2127
+ if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
2128
+ const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
2129
+ const effective = resolveWireEffort(model, resolved);
2130
+ if (effective === void 0 || effective === resolved.effort) return void 0;
2131
+ return {
2132
+ requested: resolved.effort,
2133
+ effective,
2134
+ clamped: true,
2135
+ cause: `${endpoint} does not accept reasoning effort '${resolved.effort}' for ${model}; using '${effective}'`
2136
+ };
2128
2137
  }
2129
2138
  var openaiReasoningEmit = (body, resolved, model) => {
2130
2139
  const oSeries = isOSeriesModel(model);
@@ -2149,19 +2158,21 @@ var OpenAIProvider = class extends OpenAICompatibleProvider {
2149
2158
  constructor(options = {}) {
2150
2159
  super({ profile: OPENAI_PROFILE, ...options });
2151
2160
  }
2161
+ /** Report a clamped `effort` — Chat Completions caps `'max'` at `'xhigh'` and
2162
+ * rejects `'none'` before gpt-5.1 (clamped to `'minimal'`). */
2163
+ effortResolution(options) {
2164
+ return resolveOpenAIEffortResolution(
2165
+ options,
2166
+ resolveOpenAIChatReasoningEffort,
2167
+ "OpenAI Chat Completions"
2168
+ );
2169
+ }
2152
2170
  computeCost(model, usage, _reportedCost, context) {
2153
2171
  return usage ? estimateDirectOpenAICost(model, usage, { ...context, baseUrl: this.baseUrl }) : void 0;
2154
2172
  }
2155
2173
  };
2156
2174
 
2157
2175
  // src/providers/openai-responses.ts
2158
- var OPENAI_RESPONSES_IMAGE_MODELS = /* @__PURE__ */ new Set([
2159
- "gpt-4o",
2160
- "gpt-4o-2024-08-06",
2161
- "gpt-4o-2024-11-20",
2162
- "gpt-4o-mini",
2163
- "gpt-4o-mini-2024-07-18"
2164
- ]);
2165
2176
  function base64FromSource(source) {
2166
2177
  return source.type === "base64" ? source.data : Buffer.from(source.data.buffer, source.data.byteOffset, source.data.byteLength).toString(
2167
2178
  "base64"
@@ -2200,10 +2211,17 @@ function responseImageParts(parts) {
2200
2211
  var OpenAIResponsesProvider = class {
2201
2212
  name = "openai-responses";
2202
2213
  inputCapabilities(model) {
2203
- return OPENAI_RESPONSES_IMAGE_MODELS.has(model) ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2214
+ return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2215
+ }
2216
+ /** Report a clamped `effort`. Responses accepts the native `'max'` tier that
2217
+ * Chat Completions caps, but still clamps `'none'` before gpt-5.1, `'xhigh'`
2218
+ * below gpt-5.2, and everything to `'high'` on gpt-5-pro. */
2219
+ effortResolution(options) {
2220
+ return resolveOpenAIEffortResolution(options, resolveOpenAIReasoningEffort, "OpenAI Responses");
2204
2221
  }
2205
2222
  validateInput(request) {
2206
- const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
2223
+ const modelOverride = request.providerOptions?.model;
2224
+ const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
2207
2225
  const fail = (source, feature) => {
2208
2226
  throw new UnsupportedModelInputError({
2209
2227
  provider: this.name,
@@ -2213,7 +2231,10 @@ var OpenAIResponsesProvider = class {
2213
2231
  ...feature ? { feature } : {}
2214
2232
  });
2215
2233
  };
2216
- if (!OPENAI_RESPONSES_IMAGE_MODELS.has(effectiveModel)) {
2234
+ if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
2235
+ fail(void 0, "invalid model providerOptions");
2236
+ }
2237
+ if (effectiveModel.trim().length === 0) {
2217
2238
  fail(void 0, "image input for this model");
2218
2239
  }
2219
2240
  if (request.providerOptions && "input" in request.providerOptions) {
@@ -2341,7 +2362,7 @@ var OpenAIResponsesProvider = class {
2341
2362
  const resolved = resolveThinkingOptions(options);
2342
2363
  const { includeThoughts } = resolved;
2343
2364
  const wireEffort = resolveOpenAIReasoningEffort(effectiveModel, resolved);
2344
- const stripTemp = oSeries || reasoningCapable && wireEffort !== void 0;
2365
+ const stripTemp = oSeries || supportsMaxReasoningEffort(effectiveModel) || reasoningCapable && wireEffort !== void 0;
2345
2366
  const systemMessages = messages.filter((m) => m.role === "system");
2346
2367
  const nonSystemMessages = messages.filter((m) => m.role !== "system");
2347
2368
  const body = {
@@ -2689,7 +2710,6 @@ function anthropicImageBlocks(parts) {
2689
2710
  }
2690
2711
  var ANTHROPIC_API_VERSION = "2023-06-01";
2691
2712
  var ANTHROPIC_FILES_BETA = "files-api-2025-04-14";
2692
- var ANTHROPIC_IMAGE_MODELS = /* @__PURE__ */ new Set(["claude-sonnet-4-5", "claude-opus-4-8"]);
2693
2713
  function hasAnthropicProviderFileImage(messages) {
2694
2714
  return messages.some(
2695
2715
  (message) => Array.isArray(message.content) && message.content.some(
@@ -2808,6 +2828,28 @@ var ANTHROPIC_RATES = {
2808
2828
  function isValidTokenCount(value) {
2809
2829
  return value !== void 0 && Number.isSafeInteger(value) && value >= 0;
2810
2830
  }
2831
+ function anthropicStreamErrorStatus(type) {
2832
+ switch (type) {
2833
+ case "invalid_request_error":
2834
+ return 400;
2835
+ case "authentication_error":
2836
+ return 401;
2837
+ case "permission_error":
2838
+ return 403;
2839
+ case "not_found_error":
2840
+ return 404;
2841
+ case "request_too_large":
2842
+ return 413;
2843
+ case "rate_limit_error":
2844
+ return 429;
2845
+ case "api_error":
2846
+ return 500;
2847
+ case "overloaded_error":
2848
+ return 529;
2849
+ default:
2850
+ return 0;
2851
+ }
2852
+ }
2811
2853
  function estimateAnthropicCost(model, usage, now = /* @__PURE__ */ new Date()) {
2812
2854
  if (!model || !isValidTokenCount(usage.inputTokens) || !isValidTokenCount(usage.outputTokens)) {
2813
2855
  return void 0;
@@ -2852,14 +2894,6 @@ function budgetToClaudeEffort(budget) {
2852
2894
  if (budget <= THINKING_BUDGETS.xhigh) return "xhigh";
2853
2895
  return "max";
2854
2896
  }
2855
- var warnedFableDisable = /* @__PURE__ */ new Set();
2856
- function warnFableDisable(model) {
2857
- if (warnedFableDisable.has(model)) return;
2858
- warnedFableDisable.add(model);
2859
- console.warn(
2860
- `[axl] thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'.`
2861
- );
2862
- }
2863
2897
  function resolveClaudeThinking(model, resolved) {
2864
2898
  const capability = resolveClaudeCapability(model);
2865
2899
  if (!capability) return { stripTemperature: false };
@@ -2879,11 +2913,14 @@ function resolveClaudeThinking(model, resolved) {
2879
2913
  }
2880
2914
  if (resolved.thinkingDisabled) {
2881
2915
  if (capability.thinking === "adaptive-always-on") {
2882
- warnFableDisable(model);
2883
2916
  return {
2884
2917
  thinking: { type: "adaptive" },
2885
2918
  outputConfig: { effort: "low" },
2886
- stripTemperature: true
2919
+ stripTemperature: true,
2920
+ clamp: {
2921
+ effective: "low",
2922
+ cause: `thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'`
2923
+ }
2887
2924
  };
2888
2925
  }
2889
2926
  if (activeEffort && capability.disableAt && !isEffortAtOrBelow(activeEffort, capability.disableAt)) {
@@ -2945,7 +2982,16 @@ function resolveClaudeThinking(model, resolved) {
2945
2982
  return {
2946
2983
  thinking: { type: "enabled", budget_tokens: budget },
2947
2984
  manualBudget: budget,
2948
- stripTemperature: true
2985
+ stripTemperature: true,
2986
+ // Legacy models take `budget_tokens`, not an effort level, and have no
2987
+ // `effortLevels` — so `clampAnthropicEffort` folds 'max'/'xhigh' down to
2988
+ // the 'high' tier and the request silently carries that tier's budget.
2989
+ // Report it: an unreported budget-tier downgrade is exactly the silent
2990
+ // clamp this capability exists to expose.
2991
+ clamp: activeEffort === resolved.activeEffort ? void 0 : {
2992
+ effective: activeEffort,
2993
+ cause: `Anthropic ${model} takes thinking budget_tokens rather than an effort level, and its ceiling tier is '${activeEffort}'; effort '${resolved.activeEffort}' sends budget_tokens ${budget}`
2994
+ }
2949
2995
  };
2950
2996
  }
2951
2997
  return { stripTemperature: capability.stripTemperature ?? false };
@@ -3056,11 +3102,32 @@ function mergeAnthropicUsage(initial, update) {
3056
3102
  }
3057
3103
  var AnthropicProvider = class {
3058
3104
  name = "anthropic";
3105
+ /** Report a clamped `effort`. Anthropic clamps to each model's supported
3106
+ * `output_config.effort` levels, and models that always think fall back to
3107
+ * adaptive `'low'` when asked to disable thinking. Only reported when the
3108
+ * effective level is what the request body actually carries; an explicit
3109
+ * `thinkingBudget` is documented to override `effort`, so it is not a clamp. */
3110
+ effortResolution(options) {
3111
+ const resolved = resolveThinkingOptions(options);
3112
+ if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
3113
+ const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
3114
+ const config = resolveClaudeThinking(model, resolved);
3115
+ const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
3116
+ const effective = config.clamp?.effective ?? wireEffort;
3117
+ if (effective === void 0 || effective === resolved.effort) return void 0;
3118
+ return {
3119
+ requested: resolved.effort,
3120
+ effective,
3121
+ clamped: true,
3122
+ cause: config.clamp?.cause ?? `Anthropic ${model} does not support effort '${resolved.effort}'; using '${effective}'`
3123
+ };
3124
+ }
3059
3125
  inputCapabilities(model) {
3060
- return ANTHROPIC_IMAGE_MODELS.has(model) ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
3126
+ return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
3061
3127
  }
3062
3128
  validateInput(request) {
3063
- const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
3129
+ const modelOverride = request.providerOptions?.model;
3130
+ const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
3064
3131
  const fail = (source, feature) => {
3065
3132
  throw new UnsupportedModelInputError({
3066
3133
  provider: this.name,
@@ -3070,7 +3137,10 @@ var AnthropicProvider = class {
3070
3137
  ...feature ? { feature } : {}
3071
3138
  });
3072
3139
  };
3073
- if (!ANTHROPIC_IMAGE_MODELS.has(effectiveModel)) {
3140
+ if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
3141
+ fail(void 0, "invalid model providerOptions");
3142
+ }
3143
+ if (effectiveModel.trim().length === 0) {
3074
3144
  fail(void 0, "image input for this model");
3075
3145
  }
3076
3146
  if (request.providerOptions && "messages" in request.providerOptions) {
@@ -3195,7 +3265,7 @@ var AnthropicProvider = class {
3195
3265
  if (!res.body) {
3196
3266
  throw new Error("Anthropic stream response has no body");
3197
3267
  }
3198
- yield* this.parseSSEStream(res.body, pricingContext);
3268
+ yield* this.parseSSEStream(res.body, pricingContext, res.headers);
3199
3269
  }
3200
3270
  // ---------------------------------------------------------------------------
3201
3271
  // Internal: request building
@@ -3438,7 +3508,7 @@ ${jsonInstruction}` : jsonInstruction;
3438
3508
  // ---------------------------------------------------------------------------
3439
3509
  // Internal: SSE stream parsing
3440
3510
  // ---------------------------------------------------------------------------
3441
- async *parseSSEStream(body, pricingContext) {
3511
+ async *parseSSEStream(body, pricingContext, responseHeaders) {
3442
3512
  const reader = body.getReader();
3443
3513
  const decoder = new TextDecoder();
3444
3514
  let buffer = "";
@@ -3484,6 +3554,15 @@ ${jsonInstruction}` : jsonInstruction;
3484
3554
  continue;
3485
3555
  }
3486
3556
  switch (event.type) {
3557
+ case "error": {
3558
+ const status = anthropicStreamErrorStatus(event.error?.type);
3559
+ throw buildProviderError({
3560
+ provider: this.name,
3561
+ status,
3562
+ headers: responseHeaders,
3563
+ message: event.error?.message ?? "Anthropic stream failed"
3564
+ });
3565
+ }
3487
3566
  case "content_block_start": {
3488
3567
  const block = event.content_block;
3489
3568
  if (block?.type === "tool_use") {
@@ -3735,7 +3814,6 @@ var GEMINI_PRICING = {
3735
3814
  "gemini-3.5-flash-lite": { input: 3e-7, cached: 3e-8, output: 25e-7 },
3736
3815
  "gemini-3.6-flash": { input: 15e-7, cached: 15e-8, output: 75e-7 }
3737
3816
  };
3738
- var GEMINI_INTERACTIONS_IMAGE_MODELS = /* @__PURE__ */ new Set(["gemini-3.7-flash"]);
3739
3817
  function isValidTokenCount2(value) {
3740
3818
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
3741
3819
  }
@@ -3745,6 +3823,55 @@ function isStandardGeminiRequestTier(value) {
3745
3823
  function isDefinitiveStandardGeminiResponseTier(value) {
3746
3824
  return value === "SERVICE_TIER_UNSPECIFIED" || value === "SERVICE_TIER_STANDARD" || value === "standard";
3747
3825
  }
3826
+ function geminiInteractionErrorStatus(code) {
3827
+ if (typeof code === "number" && Number.isInteger(code) && code >= 100 && code <= 599) {
3828
+ return code;
3829
+ }
3830
+ switch (code) {
3831
+ case "invalid_request":
3832
+ case "invalid_argument":
3833
+ case "parameter_unknown":
3834
+ case "failed_precondition":
3835
+ return 400;
3836
+ case "authentication":
3837
+ case "unauthenticated":
3838
+ return 401;
3839
+ case "permission_denied":
3840
+ return 403;
3841
+ case "not_found":
3842
+ case "model_not_found":
3843
+ return 404;
3844
+ case "request_timeout":
3845
+ return 408;
3846
+ case "conflict":
3847
+ case "already_exists":
3848
+ return 409;
3849
+ case "request_too_large":
3850
+ return 413;
3851
+ case "out_of_range":
3852
+ return 416;
3853
+ case "quota_exceeded":
3854
+ case "resource_exhausted":
3855
+ case "rate_limit_exceeded":
3856
+ return 429;
3857
+ case "cancelled":
3858
+ return 499;
3859
+ case "api_error":
3860
+ case "internal":
3861
+ case "internal_server_error":
3862
+ return 500;
3863
+ case "bad_gateway":
3864
+ return 502;
3865
+ case "unavailable":
3866
+ case "service_unavailable":
3867
+ return 503;
3868
+ case "deadline_exceeded":
3869
+ case "gateway_timeout":
3870
+ return 504;
3871
+ default:
3872
+ return void 0;
3873
+ }
3874
+ }
3748
3875
  function isEligibleGeminiPricing(context, hasDefinitiveStandardResponseTier, hasInvalidResponseTier) {
3749
3876
  return context.eligibleRequest && isStandardGeminiRequestTier(context.serviceTier) && hasDefinitiveStandardResponseTier && !hasInvalidResponseTier;
3750
3877
  }
@@ -3826,7 +3953,9 @@ var GEMINI_3X_MIN_THINKING_LEVEL = /* @__PURE__ */ new Map([
3826
3953
  ["gemini-3.6-flash", "minimal"],
3827
3954
  // L4 live evidence: Interactions gemini-3.7-flash accepts low/medium/high,
3828
3955
  // but rejects minimal. Keep this exact entry isolated from legacy aliases.
3829
- ["gemini-3.7-flash", "low"]
3956
+ ["gemini-3.7-flash", "low"],
3957
+ // Current-model live evidence: gemini-3.8-flash has the same low floor.
3958
+ ["gemini-3.8-flash", "low"]
3830
3959
  ]);
3831
3960
  var GEMINI_MODELS_WITHOUT_PORTABLE_TEMPERATURE = /* @__PURE__ */ new Set([
3832
3961
  "gemini-3.5-flash-lite",
@@ -3843,21 +3972,80 @@ function budgetToThinkingLevel(budgetTokens) {
3843
3972
  function minThinkingLevel(model) {
3844
3973
  return GEMINI_3X_MIN_THINKING_LEVEL.get(model) ?? "minimal";
3845
3974
  }
3846
- var _warned3xEffortNone = /* @__PURE__ */ new Set();
3847
- function warnGemini3xEffortNone(model) {
3848
- if (_warned3xEffortNone.has(model)) return;
3849
- _warned3xEffortNone.add(model);
3850
- console.warn(
3851
- `[axl] effort: 'none' on Gemini 3.x (${model}) maps to the model's minimum thinking level ('${minThinkingLevel(model)}'), not fully disabled. Gemini 3.x models cannot disable thinking entirely.`
3852
- );
3975
+ function resolveGeminiThinking(model, resolved) {
3976
+ const is3x = isGemini3x(model);
3977
+ if (resolved.thinkingDisabled) {
3978
+ const minLevel = minThinkingLevel(model);
3979
+ if (is3x) {
3980
+ return {
3981
+ interactionLevel: minLevel,
3982
+ thinkingLevel: minLevel,
3983
+ thinkingBudget: void 0,
3984
+ attachThoughts: false,
3985
+ // A `thinkingBudget: 0` disable has no unified effort to report against;
3986
+ // only an explicit `effort: 'none'` is a clamp of a requested value.
3987
+ clamp: resolved.effort === "none" ? {
3988
+ effective: minLevel,
3989
+ cause: `Gemini 3.x models cannot disable thinking; using the model minimum thinking level '${minLevel}'`
3990
+ } : void 0
3991
+ };
3992
+ }
3993
+ return {
3994
+ interactionLevel: "minimal",
3995
+ thinkingLevel: void 0,
3996
+ thinkingBudget: 0,
3997
+ attachThoughts: false
3998
+ };
3999
+ }
4000
+ if (resolved.hasBudgetOverride) {
4001
+ const budget = resolved.thinkingBudget;
4002
+ const level = budgetToThinkingLevel(budget);
4003
+ return {
4004
+ interactionLevel: level,
4005
+ thinkingLevel: is3x ? level : void 0,
4006
+ thinkingBudget: is3x ? void 0 : budget,
4007
+ attachThoughts: resolved.includeThoughts
4008
+ };
4009
+ }
4010
+ if (resolved.activeEffort) {
4011
+ const effort = resolved.activeEffort;
4012
+ const budget = THINKING_BUDGETS2[effort] ?? 5e3;
4013
+ if (is3x) {
4014
+ const level = THINKING_LEVELS[effort] ?? "medium";
4015
+ return {
4016
+ interactionLevel: level,
4017
+ thinkingLevel: level,
4018
+ thinkingBudget: void 0,
4019
+ attachThoughts: resolved.includeThoughts,
4020
+ clamp: level === effort ? void 0 : {
4021
+ effective: level,
4022
+ cause: `Gemini 3.x thinking levels cap at 'high'; effort '${effort}' maps to '${level}'`
4023
+ }
4024
+ };
4025
+ }
4026
+ return {
4027
+ interactionLevel: budgetToThinkingLevel(budget),
4028
+ thinkingLevel: void 0,
4029
+ // 2.5 Pro supports a higher max budget (32768) than other 2.5 models (24576).
4030
+ thinkingBudget: effort === "max" && model === "gemini-2.5-pro" ? 32768 : budget,
4031
+ attachThoughts: resolved.includeThoughts
4032
+ };
4033
+ }
4034
+ return {
4035
+ interactionLevel: void 0,
4036
+ thinkingLevel: void 0,
4037
+ thinkingBudget: void 0,
4038
+ attachThoughts: resolved.includeThoughts
4039
+ };
3853
4040
  }
3854
4041
  var GeminiProvider = class {
3855
4042
  name = "google";
3856
4043
  inputCapabilities(model) {
3857
- return GEMINI_INTERACTIONS_IMAGE_MODELS.has(model) ? { image: { sources: ["bytes", "base64", "provider-file"] } } : {};
4044
+ return model.trim().length > 0 ? { image: { sources: ["bytes", "base64", "provider-file"] } } : {};
3858
4045
  }
3859
4046
  validateInput(request) {
3860
- const effectiveModel = typeof request.providerOptions?.model === "string" ? request.providerOptions.model : request.model;
4047
+ const modelOverride = request.providerOptions?.model;
4048
+ const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
3861
4049
  const fail = (source, feature) => {
3862
4050
  throw new UnsupportedModelInputError({
3863
4051
  provider: this.name,
@@ -3867,7 +4055,10 @@ var GeminiProvider = class {
3867
4055
  ...feature ? { feature } : {}
3868
4056
  });
3869
4057
  };
3870
- if (!GEMINI_INTERACTIONS_IMAGE_MODELS.has(effectiveModel)) {
4058
+ if (request.providerOptions && "model" in request.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
4059
+ fail(void 0, "invalid model providerOptions");
4060
+ }
4061
+ if (effectiveModel.trim().length === 0) {
3871
4062
  fail(void 0, "image input for this model");
3872
4063
  }
3873
4064
  if (request.providerOptions) {
@@ -3914,6 +4105,21 @@ var GeminiProvider = class {
3914
4105
  nativeStructuredOutputSupport() {
3915
4106
  return "lossy";
3916
4107
  }
4108
+ /** Report a clamped `effort` (R7a). Gemini 3.x cannot disable thinking and
4109
+ * caps at `'high'`, so `'none'`, `'xhigh'` and `'max'` are not honored
4110
+ * verbatim there. Everything else — including 2.x disabled thinking — is
4111
+ * sent as asked, so there is nothing to report. */
4112
+ effortResolution(options) {
4113
+ const resolved = resolveThinkingOptions(options);
4114
+ if (resolved.effort === void 0) return void 0;
4115
+ const clamp = resolveGeminiThinking(this.requestModel(options), resolved).clamp;
4116
+ return clamp ? {
4117
+ requested: resolved.effort,
4118
+ effective: clamp.effective,
4119
+ clamped: true,
4120
+ cause: clamp.cause
4121
+ } : void 0;
4122
+ }
3917
4123
  baseUrl;
3918
4124
  apiKeySource;
3919
4125
  callCounter = 0;
@@ -4059,7 +4265,7 @@ var GeminiProvider = class {
4059
4265
  });
4060
4266
  }
4061
4267
  if (!res.body) throw new Error("Gemini Interactions stream has no body");
4062
- yield* this.parseInteractionSSEStream(res.body);
4268
+ yield* this.parseInteractionSSEStream(res.body, res.headers);
4063
4269
  }
4064
4270
  buildInteractionRequestBody(messages, options, stream) {
4065
4271
  const model = this.requestModel(options);
@@ -4088,12 +4294,9 @@ var GeminiProvider = class {
4088
4294
  generationConfig.tool_choice = typeof options.toolChoice === "string" ? options.toolChoice === "required" ? "any" : options.toolChoice : { allowed_tools: { mode: "any", tools: [options.toolChoice.function.name] } };
4089
4295
  }
4090
4296
  const thinking = resolveThinkingOptions(options);
4091
- if (thinking.thinkingDisabled) {
4092
- generationConfig.thinking_level = isGemini3x(model) ? minThinkingLevel(model) : "minimal";
4093
- } else if (thinking.hasBudgetOverride) {
4094
- generationConfig.thinking_level = budgetToThinkingLevel(thinking.thinkingBudget);
4095
- } else if (thinking.activeEffort) {
4096
- generationConfig.thinking_level = isGemini3x(model) ? THINKING_LEVELS[thinking.activeEffort] ?? "medium" : budgetToThinkingLevel(THINKING_BUDGETS2[thinking.activeEffort] ?? 5e3);
4297
+ const thinkingPlan = resolveGeminiThinking(model, thinking);
4298
+ if (thinkingPlan.interactionLevel !== void 0) {
4299
+ generationConfig.thinking_level = thinkingPlan.interactionLevel;
4097
4300
  }
4098
4301
  if (thinking.includeThoughts) generationConfig.thinking_summaries = "auto";
4099
4302
  if (Object.keys(generationConfig).length) body.generation_config = generationConfig;
@@ -4129,6 +4332,7 @@ var GeminiProvider = class {
4129
4332
  return body;
4130
4333
  }
4131
4334
  assertSafeInteractionOptions(options) {
4335
+ const modelOverride = options.providerOptions?.model;
4132
4336
  const model = this.requestModel(options);
4133
4337
  const fail = (feature) => {
4134
4338
  throw new UnsupportedModelInputError({
@@ -4138,7 +4342,10 @@ var GeminiProvider = class {
4138
4342
  feature
4139
4343
  });
4140
4344
  };
4141
- if (!GEMINI_INTERACTIONS_IMAGE_MODELS.has(model)) fail("image input for this model");
4345
+ if (options.providerOptions && "model" in options.providerOptions && (typeof modelOverride !== "string" || modelOverride.trim().length === 0)) {
4346
+ fail("invalid model providerOptions");
4347
+ }
4348
+ if (model.trim().length === 0) fail("image input for this model");
4142
4349
  const forbidden = [
4143
4350
  "input",
4144
4351
  "contents",
@@ -4214,7 +4421,7 @@ var GeminiProvider = class {
4214
4421
  providerMetadata: json.steps?.length ? { geminiInteractionSteps: json.steps.filter(isGeminiInteractionStep) } : void 0
4215
4422
  };
4216
4423
  }
4217
- async *parseInteractionSSEStream(body) {
4424
+ async *parseInteractionSSEStream(body, responseHeaders) {
4218
4425
  const reader = body.getReader();
4219
4426
  const decoder = new TextDecoder();
4220
4427
  let buffer = "";
@@ -4240,7 +4447,22 @@ var GeminiProvider = class {
4240
4447
  continue;
4241
4448
  }
4242
4449
  if (event.event_type === "error") {
4243
- throw new Error(event.error?.message ?? "Gemini Interactions stream failed");
4450
+ const status = geminiInteractionErrorStatus(event.error?.code);
4451
+ const message = event.error?.message ?? "Gemini Interactions stream failed";
4452
+ if (status !== void 0) {
4453
+ throw buildProviderError({
4454
+ provider: this.name,
4455
+ status,
4456
+ headers: responseHeaders,
4457
+ message
4458
+ });
4459
+ }
4460
+ throw new ProviderError({
4461
+ provider: this.name,
4462
+ status: 0,
4463
+ retryable: false,
4464
+ message
4465
+ });
4244
4466
  }
4245
4467
  if (event.event_type === "step.start" && event.step) {
4246
4468
  const index = event.index;
@@ -4510,50 +4732,17 @@ var GeminiProvider = class {
4510
4732
  if (Object.keys(generationConfig).length > 0) {
4511
4733
  body.generationConfig = generationConfig;
4512
4734
  }
4513
- const {
4514
- effort,
4515
- thinkingBudget,
4516
- includeThoughts,
4517
- thinkingDisabled,
4518
- activeEffort,
4519
- hasBudgetOverride
4520
- } = resolveThinkingOptions(options);
4521
- if (thinkingDisabled) {
4522
- if (isGemini3x(requestModel)) {
4523
- if (effort === "none") {
4524
- warnGemini3xEffortNone(requestModel);
4525
- }
4526
- generationConfig.thinkingConfig = { thinkingLevel: minThinkingLevel(requestModel) };
4527
- } else {
4528
- generationConfig.thinkingConfig = { thinkingBudget: 0 };
4529
- }
4530
- if (!body.generationConfig) body.generationConfig = generationConfig;
4531
- } else if (hasBudgetOverride) {
4532
- const config = {};
4533
- if (isGemini3x(requestModel)) {
4534
- config.thinkingLevel = budgetToThinkingLevel(thinkingBudget);
4535
- } else {
4536
- config.thinkingBudget = thinkingBudget;
4537
- }
4538
- if (includeThoughts) config.includeThoughts = true;
4539
- generationConfig.thinkingConfig = config;
4540
- if (!body.generationConfig) body.generationConfig = generationConfig;
4541
- } else if (activeEffort) {
4542
- const config = {};
4543
- if (isGemini3x(requestModel)) {
4544
- config.thinkingLevel = THINKING_LEVELS[activeEffort] ?? "medium";
4545
- } else {
4546
- if (activeEffort === "max" && requestModel === "gemini-2.5-pro") {
4547
- config.thinkingBudget = 32768;
4548
- } else {
4549
- config.thinkingBudget = THINKING_BUDGETS2[activeEffort] ?? 5e3;
4550
- }
4551
- }
4552
- if (includeThoughts) config.includeThoughts = true;
4553
- generationConfig.thinkingConfig = config;
4554
- if (!body.generationConfig) body.generationConfig = generationConfig;
4555
- } else if (includeThoughts) {
4556
- generationConfig.thinkingConfig = { includeThoughts: true };
4735
+ const thinkingPlan = resolveGeminiThinking(requestModel, resolveThinkingOptions(options));
4736
+ const thinkingConfig = {};
4737
+ if (thinkingPlan.thinkingLevel !== void 0) {
4738
+ thinkingConfig.thinkingLevel = thinkingPlan.thinkingLevel;
4739
+ }
4740
+ if (thinkingPlan.thinkingBudget !== void 0) {
4741
+ thinkingConfig.thinkingBudget = thinkingPlan.thinkingBudget;
4742
+ }
4743
+ if (thinkingPlan.attachThoughts) thinkingConfig.includeThoughts = true;
4744
+ if (Object.keys(thinkingConfig).length > 0) {
4745
+ generationConfig.thinkingConfig = thinkingConfig;
4557
4746
  if (!body.generationConfig) body.generationConfig = generationConfig;
4558
4747
  }
4559
4748
  if (options.toolChoice !== void 0) {
@@ -7188,6 +7377,9 @@ var AXL_EVENT_TYPES = [
7188
7377
  // structured-output cliffs (oversized appended schema, dropped refinements,
7189
7378
  // streaming disabled, zero-guidance `schemaPrompt:'none'`).
7190
7379
  "schema_diagnostic",
7380
+ // Provider capability diagnostics — the resolved provider could not honor a
7381
+ // portable request knob verbatim (first kind: a clamped `effort`).
7382
+ "provider_diagnostic",
7191
7383
  // Observability
7192
7384
  "log",
7193
7385
  "memory_remember",
@@ -7237,6 +7429,7 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
7237
7429
  "pipeline",
7238
7430
  "verify",
7239
7431
  "schema_diagnostic",
7432
+ "provider_diagnostic",
7240
7433
  "workflow_start",
7241
7434
  "workflow_end",
7242
7435
  "transcription_start",
@@ -8165,6 +8358,10 @@ var LEGACY_REDACTION_RULES = {
8165
8358
  // which is user content or PII (field paths are schema-shape, preserved like
8166
8359
  // `string_delta.path`). Passthrough, as an explicit reviewed decision.
8167
8360
  schema_diagnostic: passthrough,
8361
+ // `provider_diagnostic` carries only provider capability metadata — a provider
8362
+ // name, a model id, and two effort level strings. No application data or user
8363
+ // content ever reaches it. Passthrough, as an explicit reviewed decision.
8364
+ provider_diagnostic: passthrough,
8168
8365
  log: (e) => {
8169
8366
  if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
8170
8367
  return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
@@ -8295,6 +8492,7 @@ var COMMON_V2_REDACTION_RULES = {
8295
8492
  string_delta: reuseLegacyRule("string_delta"),
8296
8493
  verify: reuseLegacyRule("verify"),
8297
8494
  schema_diagnostic: reuseLegacyRule("schema_diagnostic"),
8495
+ provider_diagnostic: reuseLegacyRule("provider_diagnostic"),
8298
8496
  log: reuseLegacyRule("log"),
8299
8497
  memory_remember: reuseLegacyRule("memory_remember"),
8300
8498
  memory_recall: reuseLegacyRule("memory_recall"),
@@ -8390,13 +8588,14 @@ function detectDroppedRefinements(schema) {
8390
8588
  var DOCS_URL = "docs/observability.md#schema-diagnostics";
8391
8589
  var MAX_WARNED_KEYS = 2048;
8392
8590
  var warnedDiagnosticKeys = /* @__PURE__ */ new Set();
8393
- function warnSchemaDiagnosticOnce(key, message, silent) {
8591
+ function warnDiagnosticOnce(key, message, silent, docs = DOCS_URL) {
8394
8592
  if (silent || process.env.AXL_DIAGNOSTICS_SILENT === "true") return;
8395
8593
  if (warnedDiagnosticKeys.has(key)) return;
8396
8594
  if (warnedDiagnosticKeys.size >= MAX_WARNED_KEYS) warnedDiagnosticKeys.clear();
8397
8595
  warnedDiagnosticKeys.add(key);
8398
- console.warn(`[axl] ${message} See ${DOCS_URL}.`);
8596
+ console.warn(`[axl] ${message} See ${docs}.`);
8399
8597
  }
8598
+ var warnSchemaDiagnosticOnce = warnDiagnosticOnce;
8400
8599
 
8401
8600
  // src/config.ts
8402
8601
  function defineConfig(config) {
@@ -9246,7 +9445,22 @@ function appendRetryMessages(messages, content, feedbackMessage, providerMetadat
9246
9445
  content,
9247
9446
  ...providerMetadata ? { providerMetadata } : {}
9248
9447
  });
9249
- messages.push({ role: "system", content: feedbackMessage });
9448
+ messages.push({ role: "user", content: feedbackMessage });
9449
+ }
9450
+ function isRetryAbort(value) {
9451
+ return typeof value === "object" && value !== null && value.retry === false;
9452
+ }
9453
+ function describeValue(value) {
9454
+ if (value === null) return "null";
9455
+ if (Array.isArray(value)) return `an array of length ${value.length}`;
9456
+ if (typeof value === "object") {
9457
+ const keys = Object.keys(value);
9458
+ return keys.length > 0 ? `an object with keys [${keys.join(", ")}]` : "an empty object";
9459
+ }
9460
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
9461
+ return `the ${typeof value} ${JSON.stringify(value)}`;
9462
+ }
9463
+ return `a value of type ${typeof value}`;
9250
9464
  }
9251
9465
  function estimateMessagesTokens(messages) {
9252
9466
  let total = 0;
@@ -10183,6 +10397,7 @@ ${appendedSchemaText}`;
10183
10397
  provider,
10184
10398
  model
10185
10399
  });
10400
+ this.emitProviderDiagnostics(agent2, options, { provider, model, providerOptions });
10186
10401
  if (handoffMessages && handoffMessages.length > 0) {
10187
10402
  const handoffContext = handoffMessages.filter(
10188
10403
  (m) => m.role === "assistant" || m.role === "tool"
@@ -10547,7 +10762,11 @@ ${appendedSchemaText}`;
10547
10762
  retries: options.retries,
10548
10763
  metadata: options.metadata,
10549
10764
  validate: options.validate,
10550
- validateRetries: options.validateRetries
10765
+ validateRetries: options.validateRetries,
10766
+ // This seam serves both callers of a handoff: a plain `ctx.ask()` on an
10767
+ // agent with `handoffs`, and the routed `ctx.delegate()` path, whose
10768
+ // terminal ask is the handoff target. The hook travels like `validate`.
10769
+ retryFeedback: options.retryFeedback
10551
10770
  } : void 0;
10552
10771
  const handoffFn = () => askStorage.run(targetFrame, async () => {
10553
10772
  const delegateOriginal = agent2._delegateOriginalInput;
@@ -10696,9 +10915,10 @@ ${appendedSchemaText}`;
10696
10915
  const attempt = guardrailOutputRetries + 1;
10697
10916
  const maxAttempts = maxGuardrailRetries + 1;
10698
10917
  const onBlock = guardrails.onBlock ?? "throw";
10918
+ const blockReason = outputResult.reason ?? "Output blocked by guardrail";
10699
10919
  let feedbackMessage;
10700
10920
  if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
10701
- feedbackMessage = `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`;
10921
+ feedbackMessage = `Your previous response was blocked by a safety guardrail: ${blockReason}. Please provide a different response that complies with the guidelines.`;
10702
10922
  }
10703
10923
  this.emitEvent({
10704
10924
  type: "guardrail",
@@ -10721,31 +10941,43 @@ ${appendedSchemaText}`;
10721
10941
  });
10722
10942
  if (outputResult.block) {
10723
10943
  if (feedbackMessage) {
10724
- this.emitEvent({
10725
- type: "pipeline",
10726
- agent: agent2._name,
10727
- status: "failed",
10944
+ const decision = await this.resolveRetryFeedback(options?.retryFeedback, {
10728
10945
  stage: "guardrail",
10729
- attempt: guardrailOutputRetries + 1,
10730
- maxAttempts: maxGuardrailRetries + 1,
10731
- reason: feedbackMessage
10946
+ attempt,
10947
+ maxAttempts,
10948
+ output: content,
10949
+ reason: blockReason,
10950
+ defaultMessage: feedbackMessage
10732
10951
  });
10733
- guardrailOutputRetries++;
10734
- appendRetryMessages(
10735
- currentMessages,
10736
- content,
10737
- feedbackMessage,
10738
- response.providerMetadata
10739
- );
10740
- pendingRetryReason = "guardrail";
10741
- continue;
10952
+ if (decision.retry) {
10953
+ this.emitEvent({
10954
+ type: "pipeline",
10955
+ agent: agent2._name,
10956
+ status: "failed",
10957
+ stage: "guardrail",
10958
+ attempt,
10959
+ maxAttempts,
10960
+ reason: decision.message
10961
+ });
10962
+ guardrailOutputRetries++;
10963
+ appendRetryMessages(
10964
+ currentMessages,
10965
+ content,
10966
+ decision.message,
10967
+ response.providerMetadata
10968
+ );
10969
+ pendingRetryReason = "guardrail";
10970
+ continue;
10971
+ }
10972
+ this.currentSignal?.throwIfAborted();
10973
+ throw new GuardrailError("output", blockReason);
10742
10974
  }
10743
10975
  if (typeof onBlock === "function") {
10744
- return onBlock(outputResult.reason ?? "Output blocked by guardrail", {
10976
+ return onBlock(blockReason, {
10745
10977
  metadata: this.metadata
10746
10978
  });
10747
10979
  }
10748
- throw new GuardrailError("output", outputResult.reason ?? "Output blocked by guardrail");
10980
+ throw new GuardrailError("output", blockReason);
10749
10981
  }
10750
10982
  }
10751
10983
  let validated = void 0;
@@ -10765,7 +10997,7 @@ ${appendedSchemaText}`;
10765
10997
  schemaErr = err;
10766
10998
  schemaReason = err instanceof Error ? err.message : String(err);
10767
10999
  if (schemaRetries < maxSchemaRetries) {
10768
- schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Please fix and try again.`;
11000
+ schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Return a corrected response that matches the required schema.`;
10769
11001
  }
10770
11002
  }
10771
11003
  this.emitEvent({
@@ -10787,24 +11019,36 @@ ${appendedSchemaText}`;
10787
11019
  });
10788
11020
  if (!schemaValid) {
10789
11021
  if (schemaFeedback) {
10790
- this.emitEvent({
10791
- type: "pipeline",
10792
- agent: agent2._name,
10793
- status: "failed",
11022
+ const decision = await this.resolveRetryFeedback(options.retryFeedback, {
10794
11023
  stage: "schema",
10795
- attempt: schemaRetries + 1,
10796
- maxAttempts: (options.retries ?? 3) + 1,
10797
- reason: schemaFeedback
11024
+ attempt: schemaAttempt,
11025
+ maxAttempts: schemaMaxAttempts,
11026
+ output: content,
11027
+ reason: schemaReason ?? "Schema parse failed",
11028
+ ...schemaErr !== void 0 ? { error: schemaErr } : {},
11029
+ defaultMessage: schemaFeedback
10798
11030
  });
10799
- schemaRetries++;
10800
- appendRetryMessages(
10801
- currentMessages,
10802
- content,
10803
- schemaFeedback,
10804
- response.providerMetadata
10805
- );
10806
- pendingRetryReason = "schema";
10807
- continue;
11031
+ if (decision.retry) {
11032
+ this.emitEvent({
11033
+ type: "pipeline",
11034
+ agent: agent2._name,
11035
+ status: "failed",
11036
+ stage: "schema",
11037
+ attempt: schemaAttempt,
11038
+ maxAttempts: schemaMaxAttempts,
11039
+ reason: decision.message
11040
+ });
11041
+ schemaRetries++;
11042
+ appendRetryMessages(
11043
+ currentMessages,
11044
+ content,
11045
+ decision.message,
11046
+ response.providerMetadata
11047
+ );
11048
+ pendingRetryReason = "schema";
11049
+ continue;
11050
+ }
11051
+ this.currentSignal?.throwIfAborted();
10808
11052
  }
10809
11053
  const zodErr = schemaErr instanceof ZodError2 ? schemaErr : new ZodError2([
10810
11054
  {
@@ -10818,12 +11062,14 @@ ${appendedSchemaText}`;
10818
11062
  }
10819
11063
  if (options?.schema && options.validate) {
10820
11064
  let validateResult;
11065
+ let validateErr;
10821
11066
  try {
10822
11067
  validateResult = await options.validate(validated, {
10823
11068
  metadata: this.metadata
10824
11069
  });
10825
11070
  } catch (err) {
10826
11071
  rethrowEventStreamOverflow(err);
11072
+ validateErr = err;
10827
11073
  const reason = err instanceof Error ? err.message : String(err);
10828
11074
  validateResult = { valid: false, reason: `Validator error: ${reason}` };
10829
11075
  }
@@ -10832,7 +11078,7 @@ ${appendedSchemaText}`;
10832
11078
  const validateMaxAttempts = maxValidateRetries + 1;
10833
11079
  let validateFeedback;
10834
11080
  if (!validateResult.valid && validateRetries < maxValidateRetries) {
10835
- validateFeedback = `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`;
11081
+ validateFeedback = `Your previous response failed validation: ${validateResult.reason ?? "Validation failed"}. Return a corrected response that satisfies the required schema and this rule.`;
10836
11082
  }
10837
11083
  this.emitEvent({
10838
11084
  type: "validate",
@@ -10853,24 +11099,37 @@ ${appendedSchemaText}`;
10853
11099
  });
10854
11100
  if (!validateResult.valid) {
10855
11101
  if (validateFeedback) {
10856
- this.emitEvent({
10857
- type: "pipeline",
10858
- agent: agent2._name,
10859
- status: "failed",
11102
+ const decision = await this.resolveRetryFeedback(options.retryFeedback, {
10860
11103
  stage: "validate",
10861
- attempt: validateRetries + 1,
10862
- maxAttempts: (options.validateRetries ?? 2) + 1,
10863
- reason: validateFeedback
11104
+ attempt: validateAttempt,
11105
+ maxAttempts: validateMaxAttempts,
11106
+ output: content,
11107
+ parsed: validated,
11108
+ reason: validateResult.reason ?? "Validation failed",
11109
+ ...validateErr !== void 0 ? { error: validateErr } : {},
11110
+ defaultMessage: validateFeedback
10864
11111
  });
10865
- validateRetries++;
10866
- appendRetryMessages(
10867
- currentMessages,
10868
- content,
10869
- validateFeedback,
10870
- response.providerMetadata
10871
- );
10872
- pendingRetryReason = "validate";
10873
- continue;
11112
+ if (decision.retry) {
11113
+ this.emitEvent({
11114
+ type: "pipeline",
11115
+ agent: agent2._name,
11116
+ status: "failed",
11117
+ stage: "validate",
11118
+ attempt: validateAttempt,
11119
+ maxAttempts: validateMaxAttempts,
11120
+ reason: decision.message
11121
+ });
11122
+ validateRetries++;
11123
+ appendRetryMessages(
11124
+ currentMessages,
11125
+ content,
11126
+ decision.message,
11127
+ response.providerMetadata
11128
+ );
11129
+ pendingRetryReason = "validate";
11130
+ continue;
11131
+ }
11132
+ this.currentSignal?.throwIfAborted();
10874
11133
  }
10875
11134
  throw new ValidationError(
10876
11135
  validated,
@@ -10892,6 +11151,31 @@ ${appendedSchemaText}`;
10892
11151
  }
10893
11152
  throw new MaxTurnsError("ctx.ask()", maxTurns);
10894
11153
  }
11154
+ /**
11155
+ * Resolve the corrective feedback text for one rejected attempt, giving an optional
11156
+ * caller-supplied `retryFeedback` hook the chance to replace it or to stop retrying.
11157
+ *
11158
+ * Called by all three output gates, only while a retry is still permitted (never on the
11159
+ * exhausting attempt) and only after the gate's own event has been emitted, so a hook that
11160
+ * throws or aborts can never erase the evidence that the gate rejected. A non-empty string
11161
+ * replaces `defaultMessage` verbatim; `undefined` / `''` keeps it; `{ retry: false }` asks
11162
+ * the gate to throw its typed terminal error. Hook exceptions propagate — a bug in caller
11163
+ * code is not a model failure and must not be retried into silence, and neither is a
11164
+ * return value outside the contract: an unrecognized shape throws instead of silently
11165
+ * degrading to the default text, which would hide the defect behind extra token spend.
11166
+ */
11167
+ async resolveRetryFeedback(hook, info) {
11168
+ if (!hook) return { retry: true, message: info.defaultMessage };
11169
+ const result = await hook(info, { metadata: this.metadata });
11170
+ if (result === void 0 || result === "") {
11171
+ return { retry: true, message: info.defaultMessage };
11172
+ }
11173
+ if (typeof result === "string") return { retry: true, message: result };
11174
+ if (isRetryAbort(result)) return { retry: false };
11175
+ throw new TypeError(
11176
+ `retryFeedback must return a string, undefined, or { retry: false }; received ${describeValue(result)}.`
11177
+ );
11178
+ }
10895
11179
  /**
10896
11180
  * Push the final assistant message into session history, preserving providerMetadata
10897
11181
  * (e.g., Gemini thought signatures needed for multi-turn reasoning context) and
@@ -11047,6 +11331,57 @@ ${appendedSchemaText}`;
11047
11331
  }
11048
11332
  }
11049
11333
  }
11334
+ /**
11335
+ * Emit a `provider_diagnostic` when the resolved provider reports that it
11336
+ * could not honor the unified `effort` verbatim (P3/R6/R7).
11337
+ *
11338
+ * The adapter only *resolves* — it returns the requested and effective levels
11339
+ * from the same pure resolver its request builder uses. The runtime decides
11340
+ * how to surface that, because only the runtime can see the event bus and
11341
+ * `AxlConfig.diagnostics.silent` (adapters receive `ProviderConfig` only).
11342
+ *
11343
+ * Called once per ask, so a tool loop with N provider round trips still emits
11344
+ * one event; the `console.warn` dedupes once per process per distinct clamp.
11345
+ * A throwing `effortResolution` is a provider bug and propagates.
11346
+ */
11347
+ emitProviderDiagnostics(agent2, options, ctx) {
11348
+ if (!ctx.provider.effortResolution) return;
11349
+ const resolution = ctx.provider.effortResolution({
11350
+ model: ctx.model,
11351
+ effort: options?.effort ?? agent2._config.effort,
11352
+ thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
11353
+ includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
11354
+ providerOptions: ctx.providerOptions
11355
+ });
11356
+ if (!resolution?.clamped) return;
11357
+ if (typeof resolution.effective !== "string" || resolution.effective.length === 0 || typeof resolution.requested !== "string" || resolution.requested.length === 0) {
11358
+ throw new TypeError(
11359
+ `Provider '${ctx.provider.name ?? "unknown"}' returned a malformed EffortResolution for model '${ctx.model}': 'requested' and 'effective' must be non-empty strings when 'clamped' is true. Received ${JSON.stringify(resolution)}.`
11360
+ );
11361
+ }
11362
+ const cause = resolution.cause ?? `effort '${resolution.requested}' is not supported as asked`;
11363
+ this.emitEvent({
11364
+ type: "provider_diagnostic",
11365
+ agent: agent2._name,
11366
+ data: {
11367
+ kind: "effort_clamped",
11368
+ ...ctx.provider.name ? { provider: ctx.provider.name } : {},
11369
+ model: ctx.model,
11370
+ requested: resolution.requested,
11371
+ effective: resolution.effective,
11372
+ cause
11373
+ }
11374
+ });
11375
+ warnDiagnosticOnce(
11376
+ // One warning per distinct clamp, not per ask: the same clamp across 500
11377
+ // asks is one process-level surprise, while a different requested/effective
11378
+ // pair is a genuinely new one.
11379
+ `effort_clamped\0${ctx.provider.name ?? ""}\0${ctx.model}\0${resolution.requested}\0${resolution.effective}`,
11380
+ `effort '${resolution.requested}' was clamped to '${resolution.effective}' on ${ctx.provider.name ? `${ctx.provider.name} ` : ""}${ctx.model} \u2014 ${cause}. A 'provider_diagnostic' event carries the same detail per ask.`,
11381
+ this.config.diagnostics?.silent,
11382
+ "docs/observability.md#provider-diagnostics"
11383
+ );
11384
+ }
11050
11385
  buildToolDefs(agent2, resolvedHandoffs) {
11051
11386
  const defs = [];
11052
11387
  if (agent2._config.tools) {
@@ -12327,7 +12662,8 @@ ${summaryResponse.content}`
12327
12662
  retries: options?.retries,
12328
12663
  metadata: options?.metadata,
12329
12664
  validate: options?.validate,
12330
- validateRetries: options?.validateRetries
12665
+ validateRetries: options?.validateRetries,
12666
+ retryFeedback: options?.retryFeedback
12331
12667
  });
12332
12668
  }
12333
12669
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
@@ -12375,7 +12711,8 @@ ${summaryResponse.content}`
12375
12711
  retries: options?.retries,
12376
12712
  metadata: options?.metadata,
12377
12713
  validate: options?.validate,
12378
- validateRetries: options?.validateRetries
12714
+ validateRetries: options?.validateRetries,
12715
+ retryFeedback: options?.retryFeedback
12379
12716
  });
12380
12717
  }
12381
12718
  /** Transcribe recorded audio through the dedicated transcription registry.