port(pi-mono): normalize Bedrock model names for inference profiles (refs ed4bc7308)

Pi-mono Tier 0 #5 — first sf-driven port. sf-from-source dispatched the
task in print mode and produced this fix autonomously.

Adds getModelMatchCandidates(modelId, modelName?) helper that normalizes
both inputs to lowercase and dash-separated form
(s.replace(/[\s_.:]+/g, "-")). Inference profile ARNs don't embed the
model name; the helper lets capability checks match against either the
inference profile ARN or the underlying model name.

Updated:
- supportsAdaptiveThinking — uses the helper; consolidates the
  opus-4.6/opus-4-6 dot-vs-dash variants.
- mapThinkingLevelToEffort — same pattern.
- supportsPromptCaching — same pattern (also from pi-mono PR #3527).
- streamSimpleBedrock and buildAdditionalModelRequestFields — pass
  model.name through to capability checks.

Type-check passes (cd packages/pi-ai && npx tsc --noEmit).

Co-Authored-By: sf v2.75.1 (session 911dd2de)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-04-29 14:14:17 +02:00
parent a3c487c918
commit 7c487bb60e

View file

@ -230,7 +230,7 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp
const effectiveReasoning = resolveReasoningLevel(model, options.reasoning);
if (model.id.includes("anthropic.claude") || model.id.includes("anthropic/claude")) {
if (supportsAdaptiveThinking(model.id) && isAutoReasoning(options.reasoning)) {
if (supportsAdaptiveThinking(model.id, model.name) && isAutoReasoning(options.reasoning)) {
return streamBedrock(model, context, {
...base,
reasoning: options.reasoning,
@ -238,7 +238,7 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp
} satisfies BedrockOptions);
}
if (supportsAdaptiveThinking(model.id)) {
if (supportsAdaptiveThinking(model.id, model.name)) {
return streamBedrock(model, context, {
...base,
reasoning: effectiveReasoning,
@ -393,22 +393,32 @@ function handleContentBlockStop(
}
}
/**
* Checks both model ID and model name to support application inference profiles
* whose ARNs don't contain the model name.
*/
function getModelMatchCandidates(modelId: string, modelName?: string): string[] {
const values = modelName ? [modelId, modelName] : [modelId];
return values.flatMap((value) => {
const lower = value.toLowerCase();
return [lower, lower.replace(/[\s_.:]+/g, "-")];
});
}
/**
* Check if the model supports adaptive thinking (Opus 4.6/4.7, Sonnet 4.6/4.7, Haiku 4.5).
* @internal exported for testing only
*/
export function supportsAdaptiveThinking(modelId: string): boolean {
export function supportsAdaptiveThinking(modelId: string, modelName?: string): boolean {
const candidates = getModelMatchCandidates(modelId, modelName);
return (
modelId.includes("opus-4-6") ||
modelId.includes("opus-4.6") ||
modelId.includes("opus-4-7") ||
modelId.includes("opus-4.7") ||
modelId.includes("sonnet-4-6") ||
modelId.includes("sonnet-4.6") ||
modelId.includes("sonnet-4-7") ||
modelId.includes("sonnet-4.7") ||
modelId.includes("haiku-4-5") ||
modelId.includes("haiku-4.5")
candidates.some((s) =>
s.includes("opus-4-6") ||
s.includes("opus-4-7") ||
s.includes("sonnet-4-6") ||
s.includes("sonnet-4-7") ||
s.includes("haiku-4-5"),
)
);
}
@ -416,7 +426,9 @@ export function supportsAdaptiveThinking(modelId: string): boolean {
export function mapThinkingLevelToEffort(
level: SimpleStreamOptions["reasoning"],
modelId: string,
modelName?: string,
): "low" | "medium" | "high" | "xhigh" | "max" {
const candidates = getModelMatchCandidates(modelId, modelName);
switch (level) {
case "auto":
return "medium";
@ -428,8 +440,8 @@ export function mapThinkingLevelToEffort(
case "high":
return "high";
case "xhigh":
if (modelId.includes("opus-4-7") || modelId.includes("opus-4.7")) return "xhigh";
if (modelId.includes("opus-4-6") || modelId.includes("opus-4.6")) return "max";
if (candidates.some((s) => s.includes("opus-4-7"))) return "xhigh";
if (candidates.some((s) => s.includes("opus-4-6"))) return "max";
return "high";
default:
return "high";
@ -459,13 +471,17 @@ function supportsPromptCaching(model: Model<"bedrock-converse-stream">): boolean
return true;
}
const id = model.id.toLowerCase();
const candidates = getModelMatchCandidates(model.id, model.name);
const hasClaudeRef = candidates.some((s) => s.includes("claude"));
if (!hasClaudeRef) {
return false;
}
// Claude 4.x models (opus-4, sonnet-4, haiku-4)
if (id.includes("claude") && (id.includes("-4-") || id.includes("-4."))) return true;
if (candidates.some((s) => s.includes("-4-"))) return true;
// Claude 3.7 Sonnet
if (id.includes("claude-3-7-sonnet")) return true;
if (candidates.some((s) => s.includes("claude-3-7-sonnet"))) return true;
// Claude 3.5 Haiku
if (id.includes("claude-3-5-haiku")) return true;
if (candidates.some((s) => s.includes("claude-3-5-haiku"))) return true;
return false;
}
@ -721,14 +737,14 @@ export function buildAdditionalModelRequestFields(
}
if (model.id.includes("anthropic.claude") || model.id.includes("anthropic/claude")) {
const result: Record<string, any> = supportsAdaptiveThinking(model.id)
const result: Record<string, any> = supportsAdaptiveThinking(model.id, model.name)
? options.reasoning === "auto"
? {
thinking: { type: "adaptive" },
}
: {
thinking: { type: "adaptive" },
output_config: { effort: mapThinkingLevelToEffort(options.reasoning, model.id) },
output_config: { effort: mapThinkingLevelToEffort(options.reasoning, model.id, model.name) },
}
: (() => {
const defaultBudgets: Record<ThinkingLevel, number> = {
@ -752,7 +768,7 @@ export function buildAdditionalModelRequestFields(
};
})();
if (!supportsAdaptiveThinking(model.id) && (options.interleavedThinking ?? true)) {
if (!supportsAdaptiveThinking(model.id, model.name) && (options.interleavedThinking ?? true)) {
result.anthropic_beta = ["interleaved-thinking-2025-05-14"];
}