fix(providers): apply auth patch deletions

This commit is contained in:
Vincent Koc
2026-06-10 16:35:55 +09:00
parent 06312ad805
commit 4afe616f22
6 changed files with 113 additions and 23 deletions

View File

@@ -167,7 +167,22 @@ describe("Bedrock profile endpoint resolution", () => {
});
describe("Bedrock thinking effort mapping", () => {
it("caps max effort at high for Claude Sonnet 4.6", () => {
it("forces adaptive thinking for mandatory Claude models when callers omit reasoning", () => {
const model = bedrockModel({
id: "anthropic.claude-sonnet-4-6-v1:0",
name: "Claude Sonnet 4.6",
reasoning: true,
});
const options = testing.resolveSimpleBedrockOptions(model, {});
expect(options.reasoning).toBe("high");
expect(testing.buildAdditionalModelRequestFields(model, options)).toEqual({
thinking: { type: "adaptive", display: "summarized" },
output_config: { effort: "high" },
});
});
it("clamps max effort for Claude models without native max support", () => {
expect(
testing.mapThinkingLevelToEffort(
bedrockModel({

View File

@@ -53,6 +53,7 @@ import {
type ToolResultMessage,
} from "openclaw/plugin-sdk/llm";
import {
isClaudeAdaptiveThinkingDefaultModelId,
resolveClaudeFable5ModelIdentity,
resolveClaudeModelIdentity,
supportsClaudeAdaptiveThinking,
@@ -351,29 +352,38 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp
model: Model<"bedrock-converse-stream">,
context: Context,
options?: SimpleStreamOptions,
) => {
) => streamBedrock(model, context, resolveSimpleBedrockOptions(model, options));
function resolveSimpleBedrockOptions(
model: Model<"bedrock-converse-stream">,
options?: SimpleStreamOptions,
): BedrockOptions {
const base = buildBaseOptions(model, options, undefined);
if (usesClaudeFable5BedrockContract(model)) {
return streamBedrock(model, context, {
return {
...base,
reasoning: options?.reasoning ?? "high",
thinkingBudgets: options?.thinkingBudgets,
} satisfies BedrockOptions);
} satisfies BedrockOptions;
}
if (!options?.reasoning) {
return streamBedrock(model, context, {
const reasoning =
isAnthropicClaudeModel(model) && requiresMandatoryAdaptiveThinking(model)
? "high"
: undefined;
return {
...base,
reasoning: undefined,
} satisfies BedrockOptions);
reasoning,
} satisfies BedrockOptions;
}
if (isAnthropicClaudeModel(model)) {
if (supportsAdaptiveThinking(model)) {
return streamBedrock(model, context, {
return {
...base,
reasoning: options.reasoning,
thinkingBudgets: options.thinkingBudgets,
} satisfies BedrockOptions);
} satisfies BedrockOptions;
}
// Undefined means the caller did not request an output cap; let the helper use the model cap.
@@ -385,7 +395,7 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp
options.thinkingBudgets,
);
return streamBedrock(model, context, {
return {
...base,
maxTokens: adjusted.maxTokens,
reasoning: options.reasoning,
@@ -393,15 +403,15 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp
...options.thinkingBudgets,
[clampReasoning(options.reasoning)!]: adjusted.thinkingBudget,
},
} satisfies BedrockOptions);
} satisfies BedrockOptions;
}
return streamBedrock(model, context, {
return {
...base,
reasoning: options.reasoning,
thinkingBudgets: options.thinkingBudgets,
} satisfies BedrockOptions);
};
} satisfies BedrockOptions;
}
function handleContentBlockStart(
event: ContentBlockStartEvent,
@@ -565,6 +575,14 @@ function supportsAdaptiveThinking(model: Model<"bedrock-converse-stream">): bool
);
}
function requiresMandatoryAdaptiveThinking(model: Model<"bedrock-converse-stream">): boolean {
const profileModelId = resolveClaudeProfileNameModelId(model.name);
return (
isClaudeAdaptiveThinkingDefaultModelId(resolveClaudeModelIdentity(model)) ||
(profileModelId ? isClaudeAdaptiveThinkingDefaultModelId(profileModelId) : false)
);
}
function supportsNativeXhighEffort(model: Model<"bedrock-converse-stream">): boolean {
const profileModelId = resolveClaudeProfileNameModelId(model.name);
return (
@@ -1071,9 +1089,11 @@ function createImageBlock(mimeType: string, data: string) {
/** Test-only hooks for Bedrock runtime conversion and endpoint policy. */
export const testing = {
buildAdditionalModelRequestFields,
convertMessages,
getConfiguredBedrockRegion,
hasConfiguredBedrockProfile,
mapThinkingLevelToEffort,
resolveSimpleBedrockOptions,
shouldUseExplicitBedrockEndpoint,
};

View File

@@ -1307,11 +1307,11 @@ describe("microsoft-foundry plugin", () => {
const provider = result.configPatch?.models?.providers?.["microsoft-foundry"] as
| Record<string, unknown>
| undefined;
expect(provider).toMatchObject({
authHeader: true,
apiKey: null,
headers: null,
});
expect(provider?.authHeader).toBe(true);
expect(Object.hasOwn(provider ?? {}, "apiKey")).toBe(true);
expect(Object.hasOwn(provider ?? {}, "headers")).toBe(true);
expect(provider?.apiKey).toBeUndefined();
expect(provider?.headers).toBeUndefined();
});
it.each([

View File

@@ -111,8 +111,8 @@ type FoundryModelCapabilities = {
};
type FoundryProviderConfigPatch = Omit<ModelProviderConfig, "apiKey" | "headers"> & {
apiKey?: SecretInput | null;
headers?: Record<string, SecretInput> | null;
apiKey?: SecretInput | undefined;
headers?: Record<string, SecretInput> | undefined;
};
function normalizeModelInput(input?: unknown): Array<"text" | "image"> {
@@ -481,7 +481,7 @@ function buildFoundryProviderConfig(
: {}),
}
: isEntraIdAuth
? { authHeader: true, apiKey: null, headers: null }
? { authHeader: true, apiKey: undefined, headers: undefined }
: {}),
models: deployments.map((deployment) => {
const capabilities = resolveFoundryModelCapabilities(

View File

@@ -118,6 +118,43 @@ describe("applyProviderAuthConfigPatch", () => {
});
});
it("deletes provider auth fields marked undefined by auth patches", () => {
const baseLocal = {
models: {
providers: {
"microsoft-foundry": {
baseUrl: "https://example.services.ai.azure.com/openai/v1",
api: "anthropic-messages",
authHeader: false,
apiKey: "FOUNDRY_API_KEY",
headers: { "api-key": "FOUNDRY_API_KEY" },
models: [],
},
},
},
} satisfies OpenClawConfig;
const patch = {
models: {
providers: {
"microsoft-foundry": {
authHeader: true,
apiKey: undefined,
headers: undefined,
},
},
},
};
const next = applyProviderAuthConfigPatch(baseLocal, patch);
const provider = next.models?.providers?.["microsoft-foundry"] as
| Record<string, unknown>
| undefined;
expect(provider).toMatchObject({ authHeader: true });
expect(provider).not.toHaveProperty("apiKey");
expect(provider).not.toHaveProperty("headers");
});
it("normalizes retired Google Gemini model refs from provider config patches", () => {
const patch = {
agents: {

View File

@@ -94,6 +94,22 @@ function mergeConfigPatch<T>(base: T, patch: unknown): T {
return next as T;
}
function deleteUndefinedPatchLeaves<T>(target: T, patch: unknown): T {
if (!isPlainRecord(target) || !isPlainRecord(patch)) {
return target;
}
const targetRecord = target as Record<string, unknown>;
for (const [key, value] of Object.entries(patch)) {
if (value === undefined) {
delete targetRecord[key];
continue;
}
deleteUndefinedPatchLeaves(targetRecord[key], value);
}
return target;
}
function normalizeAgentModelConfigForWrite(value: unknown): unknown {
if (typeof value === "string") {
return normalizeAgentModelRefForConfig(value);
@@ -259,7 +275,9 @@ export function applyProviderAuthConfigPatch(
patch: unknown,
options?: { replaceDefaultModels?: boolean },
): OpenClawConfig {
const merged = normalizeConfigModelRefsForWrite(mergeConfigPatch(cfg, patch));
const merged = normalizeConfigModelRefsForWrite(
deleteUndefinedPatchLeaves(mergeConfigPatch(cfg, patch), patch),
);
if (!options?.replaceDefaultModels || !isPlainRecord(patch)) {
return merged;
}