Skip to content

Commit 4afe616

Browse files
committed
fix(providers): apply auth patch deletions
1 parent 06312ad commit 4afe616

6 files changed

Lines changed: 113 additions & 23 deletions

File tree

extensions/amazon-bedrock/stream.runtime.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,22 @@ describe("Bedrock profile endpoint resolution", () => {
167167
});
168168

169169
describe("Bedrock thinking effort mapping", () => {
170-
it("caps max effort at high for Claude Sonnet 4.6", () => {
170+
it("forces adaptive thinking for mandatory Claude models when callers omit reasoning", () => {
171+
const model = bedrockModel({
172+
id: "anthropic.claude-sonnet-4-6-v1:0",
173+
name: "Claude Sonnet 4.6",
174+
reasoning: true,
175+
});
176+
const options = testing.resolveSimpleBedrockOptions(model, {});
177+
178+
expect(options.reasoning).toBe("high");
179+
expect(testing.buildAdditionalModelRequestFields(model, options)).toEqual({
180+
thinking: { type: "adaptive", display: "summarized" },
181+
output_config: { effort: "high" },
182+
});
183+
});
184+
185+
it("clamps max effort for Claude models without native max support", () => {
171186
expect(
172187
testing.mapThinkingLevelToEffort(
173188
bedrockModel({

extensions/amazon-bedrock/stream.runtime.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
type ToolResultMessage,
5454
} from "openclaw/plugin-sdk/llm";
5555
import {
56+
isClaudeAdaptiveThinkingDefaultModelId,
5657
resolveClaudeFable5ModelIdentity,
5758
resolveClaudeModelIdentity,
5859
supportsClaudeAdaptiveThinking,
@@ -351,29 +352,38 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp
351352
model: Model<"bedrock-converse-stream">,
352353
context: Context,
353354
options?: SimpleStreamOptions,
354-
) => {
355+
) => streamBedrock(model, context, resolveSimpleBedrockOptions(model, options));
356+
357+
function resolveSimpleBedrockOptions(
358+
model: Model<"bedrock-converse-stream">,
359+
options?: SimpleStreamOptions,
360+
): BedrockOptions {
355361
const base = buildBaseOptions(model, options, undefined);
356362
if (usesClaudeFable5BedrockContract(model)) {
357-
return streamBedrock(model, context, {
363+
return {
358364
...base,
359365
reasoning: options?.reasoning ?? "high",
360366
thinkingBudgets: options?.thinkingBudgets,
361-
} satisfies BedrockOptions);
367+
} satisfies BedrockOptions;
362368
}
363369
if (!options?.reasoning) {
364-
return streamBedrock(model, context, {
370+
const reasoning =
371+
isAnthropicClaudeModel(model) && requiresMandatoryAdaptiveThinking(model)
372+
? "high"
373+
: undefined;
374+
return {
365375
...base,
366-
reasoning: undefined,
367-
} satisfies BedrockOptions);
376+
reasoning,
377+
} satisfies BedrockOptions;
368378
}
369379

370380
if (isAnthropicClaudeModel(model)) {
371381
if (supportsAdaptiveThinking(model)) {
372-
return streamBedrock(model, context, {
382+
return {
373383
...base,
374384
reasoning: options.reasoning,
375385
thinkingBudgets: options.thinkingBudgets,
376-
} satisfies BedrockOptions);
386+
} satisfies BedrockOptions;
377387
}
378388

379389
// Undefined means the caller did not request an output cap; let the helper use the model cap.
@@ -385,23 +395,23 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp
385395
options.thinkingBudgets,
386396
);
387397

388-
return streamBedrock(model, context, {
398+
return {
389399
...base,
390400
maxTokens: adjusted.maxTokens,
391401
reasoning: options.reasoning,
392402
thinkingBudgets: {
393403
...options.thinkingBudgets,
394404
[clampReasoning(options.reasoning)!]: adjusted.thinkingBudget,
395405
},
396-
} satisfies BedrockOptions);
406+
} satisfies BedrockOptions;
397407
}
398408

399-
return streamBedrock(model, context, {
409+
return {
400410
...base,
401411
reasoning: options.reasoning,
402412
thinkingBudgets: options.thinkingBudgets,
403-
} satisfies BedrockOptions);
404-
};
413+
} satisfies BedrockOptions;
414+
}
405415

406416
function handleContentBlockStart(
407417
event: ContentBlockStartEvent,
@@ -565,6 +575,14 @@ function supportsAdaptiveThinking(model: Model<"bedrock-converse-stream">): bool
565575
);
566576
}
567577

578+
function requiresMandatoryAdaptiveThinking(model: Model<"bedrock-converse-stream">): boolean {
579+
const profileModelId = resolveClaudeProfileNameModelId(model.name);
580+
return (
581+
isClaudeAdaptiveThinkingDefaultModelId(resolveClaudeModelIdentity(model)) ||
582+
(profileModelId ? isClaudeAdaptiveThinkingDefaultModelId(profileModelId) : false)
583+
);
584+
}
585+
568586
function supportsNativeXhighEffort(model: Model<"bedrock-converse-stream">): boolean {
569587
const profileModelId = resolveClaudeProfileNameModelId(model.name);
570588
return (
@@ -1071,9 +1089,11 @@ function createImageBlock(mimeType: string, data: string) {
10711089

10721090
/** Test-only hooks for Bedrock runtime conversion and endpoint policy. */
10731091
export const testing = {
1092+
buildAdditionalModelRequestFields,
10741093
convertMessages,
10751094
getConfiguredBedrockRegion,
10761095
hasConfiguredBedrockProfile,
10771096
mapThinkingLevelToEffort,
1097+
resolveSimpleBedrockOptions,
10781098
shouldUseExplicitBedrockEndpoint,
10791099
};

extensions/microsoft-foundry/index.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,11 +1307,11 @@ describe("microsoft-foundry plugin", () => {
13071307
const provider = result.configPatch?.models?.providers?.["microsoft-foundry"] as
13081308
| Record<string, unknown>
13091309
| undefined;
1310-
expect(provider).toMatchObject({
1311-
authHeader: true,
1312-
apiKey: null,
1313-
headers: null,
1314-
});
1310+
expect(provider?.authHeader).toBe(true);
1311+
expect(Object.hasOwn(provider ?? {}, "apiKey")).toBe(true);
1312+
expect(Object.hasOwn(provider ?? {}, "headers")).toBe(true);
1313+
expect(provider?.apiKey).toBeUndefined();
1314+
expect(provider?.headers).toBeUndefined();
13151315
});
13161316

13171317
it.each([

extensions/microsoft-foundry/shared.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ type FoundryModelCapabilities = {
111111
};
112112

113113
type FoundryProviderConfigPatch = Omit<ModelProviderConfig, "apiKey" | "headers"> & {
114-
apiKey?: SecretInput | null;
115-
headers?: Record<string, SecretInput> | null;
114+
apiKey?: SecretInput | undefined;
115+
headers?: Record<string, SecretInput> | undefined;
116116
};
117117

118118
function normalizeModelInput(input?: unknown): Array<"text" | "image"> {
@@ -481,7 +481,7 @@ function buildFoundryProviderConfig(
481481
: {}),
482482
}
483483
: isEntraIdAuth
484-
? { authHeader: true, apiKey: null, headers: null }
484+
? { authHeader: true, apiKey: undefined, headers: undefined }
485485
: {}),
486486
models: deployments.map((deployment) => {
487487
const capabilities = resolveFoundryModelCapabilities(

src/plugins/provider-auth-choice-helpers.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,43 @@ describe("applyProviderAuthConfigPatch", () => {
118118
});
119119
});
120120

121+
it("deletes provider auth fields marked undefined by auth patches", () => {
122+
const baseLocal = {
123+
models: {
124+
providers: {
125+
"microsoft-foundry": {
126+
baseUrl: "https://example.services.ai.azure.com/openai/v1",
127+
api: "anthropic-messages",
128+
authHeader: false,
129+
apiKey: "FOUNDRY_API_KEY",
130+
headers: { "api-key": "FOUNDRY_API_KEY" },
131+
models: [],
132+
},
133+
},
134+
},
135+
} satisfies OpenClawConfig;
136+
const patch = {
137+
models: {
138+
providers: {
139+
"microsoft-foundry": {
140+
authHeader: true,
141+
apiKey: undefined,
142+
headers: undefined,
143+
},
144+
},
145+
},
146+
};
147+
148+
const next = applyProviderAuthConfigPatch(baseLocal, patch);
149+
const provider = next.models?.providers?.["microsoft-foundry"] as
150+
| Record<string, unknown>
151+
| undefined;
152+
153+
expect(provider).toMatchObject({ authHeader: true });
154+
expect(provider).not.toHaveProperty("apiKey");
155+
expect(provider).not.toHaveProperty("headers");
156+
});
157+
121158
it("normalizes retired Google Gemini model refs from provider config patches", () => {
122159
const patch = {
123160
agents: {

src/plugins/provider-auth-choice-helpers.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,22 @@ function mergeConfigPatch<T>(base: T, patch: unknown): T {
9494
return next as T;
9595
}
9696

97+
function deleteUndefinedPatchLeaves<T>(target: T, patch: unknown): T {
98+
if (!isPlainRecord(target) || !isPlainRecord(patch)) {
99+
return target;
100+
}
101+
102+
const targetRecord = target as Record<string, unknown>;
103+
for (const [key, value] of Object.entries(patch)) {
104+
if (value === undefined) {
105+
delete targetRecord[key];
106+
continue;
107+
}
108+
deleteUndefinedPatchLeaves(targetRecord[key], value);
109+
}
110+
return target;
111+
}
112+
97113
function normalizeAgentModelConfigForWrite(value: unknown): unknown {
98114
if (typeof value === "string") {
99115
return normalizeAgentModelRefForConfig(value);
@@ -259,7 +275,9 @@ export function applyProviderAuthConfigPatch(
259275
patch: unknown,
260276
options?: { replaceDefaultModels?: boolean },
261277
): OpenClawConfig {
262-
const merged = normalizeConfigModelRefsForWrite(mergeConfigPatch(cfg, patch));
278+
const merged = normalizeConfigModelRefsForWrite(
279+
deleteUndefinedPatchLeaves(mergeConfigPatch(cfg, patch), patch),
280+
);
263281
if (!options?.replaceDefaultModels || !isPlainRecord(patch)) {
264282
return merged;
265283
}

0 commit comments

Comments
 (0)