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); const effectiveReasoning = resolveReasoningLevel(model, options.reasoning);
if (model.id.includes("anthropic.claude") || model.id.includes("anthropic/claude")) { 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, { return streamBedrock(model, context, {
...base, ...base,
reasoning: options.reasoning, reasoning: options.reasoning,
@ -238,7 +238,7 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp
} satisfies BedrockOptions); } satisfies BedrockOptions);
} }
if (supportsAdaptiveThinking(model.id)) { if (supportsAdaptiveThinking(model.id, model.name)) {
return streamBedrock(model, context, { return streamBedrock(model, context, {
...base, ...base,
reasoning: effectiveReasoning, 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). * 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 * @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 ( return (
modelId.includes("opus-4-6") || candidates.some((s) =>
modelId.includes("opus-4.6") || s.includes("opus-4-6") ||
modelId.includes("opus-4-7") || s.includes("opus-4-7") ||
modelId.includes("opus-4.7") || s.includes("sonnet-4-6") ||
modelId.includes("sonnet-4-6") || s.includes("sonnet-4-7") ||
modelId.includes("sonnet-4.6") || s.includes("haiku-4-5"),
modelId.includes("sonnet-4-7") || )
modelId.includes("sonnet-4.7") ||
modelId.includes("haiku-4-5") ||
modelId.includes("haiku-4.5")
); );
} }
@ -416,7 +426,9 @@ export function supportsAdaptiveThinking(modelId: string): boolean {
export function mapThinkingLevelToEffort( export function mapThinkingLevelToEffort(
level: SimpleStreamOptions["reasoning"], level: SimpleStreamOptions["reasoning"],
modelId: string, modelId: string,
modelName?: string,
): "low" | "medium" | "high" | "xhigh" | "max" { ): "low" | "medium" | "high" | "xhigh" | "max" {
const candidates = getModelMatchCandidates(modelId, modelName);
switch (level) { switch (level) {
case "auto": case "auto":
return "medium"; return "medium";
@ -428,8 +440,8 @@ export function mapThinkingLevelToEffort(
case "high": case "high":
return "high"; return "high";
case "xhigh": case "xhigh":
if (modelId.includes("opus-4-7") || modelId.includes("opus-4.7")) return "xhigh"; if (candidates.some((s) => s.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-6"))) return "max";
return "high"; return "high";
default: default:
return "high"; return "high";
@ -459,13 +471,17 @@ function supportsPromptCaching(model: Model<"bedrock-converse-stream">): boolean
return true; 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) // 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 // 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 // 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; return false;
} }
@ -721,14 +737,14 @@ export function buildAdditionalModelRequestFields(
} }
if (model.id.includes("anthropic.claude") || model.id.includes("anthropic/claude")) { 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" ? options.reasoning === "auto"
? { ? {
thinking: { type: "adaptive" }, thinking: { type: "adaptive" },
} }
: { : {
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> = { 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"]; result.anthropic_beta = ["interleaved-thinking-2025-05-14"];
} }