{"version":3,"file":"model-utils.cjs","names":[],"sources":["../src/model-utils.ts"],"sourcesContent":["const DATE_SUFFIX_RE = /[-](\\d{8}(-v\\d+([:.]\\d+)*)?)$|[-]\\d{4}-\\d{2}-\\d{2}$/;\n\nexport function normalizeModelName(\n  model: string | undefined,\n  skipNormalization?: boolean,\n): string | undefined {\n  if (!model || skipNormalization) return model;\n  return model.replace(DATE_SUFFIX_RE, \"\");\n}\n\n// ─── Reasoning-capability map ───────────────────────────────────────────────\n//\n// `isReasoningModel` answers: \"would the REAL provider for this model id emit a\n// reasoning/thinking channel?\" aimock uses it to gate synthesized reasoning so a\n// fixture's `reasoning` field is not replayed for a model that cannot reason\n// (the gpt-4.1 false-green described in aimock#254).\n//\n// Policy (see the #254 spec for the full rationale):\n//   - The KNOWN-NON-REASONING list (NONREASONING_FAMILIES) is the load-bearing\n//     guard. A match there → false.\n//   - The reasoning list (REASONING_FAMILIES) is mostly documentation; because\n//     unknown ids default to `true` it is not load-bearing for correctness.\n//   - UNKNOWN id → true (assume reasoning-capable). A false negative silently\n//     breaks legitimate replays and is hard to notice; a false positive merely\n//     fails to catch one mis-wiring class. Failing open keeps the blast radius\n//     small and bounds maintenance to the explicit non-reasoning list.\n//\n// Env overrides let users correct drift without a release:\n//   AIMOCK_REASONING_MODELS     — comma-separated ids/prefixes forced capable.\n//   AIMOCK_NONREASONING_MODELS  — comma-separated ids/prefixes forced incapable.\n// Env entries are normalized IDENTICALLY to the incoming model id (date suffix\n// + provider/region prefix stripped, lowercased) so a prefixed entry such as\n// `anthropic.claude-opus-4` matches the already-stripped id.\n//\n// Precedence is BETWEEN lists, enforced by the sequential `if` checks below:\n//   env-nonreasoning > env-reasoning > built-in-nonreasoning > built-in-reasoning > unknown(true)\n// Within any single list the result is order-INDEPENDENT — `matchesAny` uses\n// `Array.some(startsWith)`, so any prefix match yields that list's verdict.\n\n// Known non-reasoning families — the load-bearing denylist. Entries are matched\n// as prefixes against the normalized, lowercased, provider-stripped model id.\n// Order within this list does not matter (see `matchesAny`): any prefix that\n// matches the id produces `false`.\nconst NONREASONING_FAMILIES = [\n  \"gpt-4.1\", // the exact model from aimock#254 (covers -mini / -nano)\n  \"gpt-4o\", // covers gpt-4o-mini\n  \"gpt-4-turbo\",\n  \"gpt-4\", // plain gpt-4; order-independent, and prefix-safe since longer ids (gpt-4.1/gpt-4o/gpt-4-turbo) are also denylisted\n  \"gpt-3.5\",\n  \"claude-3-5-\", // Sonnet/Haiku 3.5 — no extended thinking\n  \"claude-3-haiku\",\n  \"claude-3-opus\",\n  \"claude-3-sonnet\",\n  \"gemini-1.5-\",\n];\n\n// Reasoning-capable families (informational; unknown already defaults to true).\nconst REASONING_FAMILIES = [\n  \"o1\",\n  \"o3\",\n  \"o4\",\n  \"gpt-5\",\n  \"deepseek-r1\",\n  \"deepseek-reasoner\",\n  \"claude-3-7-sonnet\",\n  \"claude-opus-4\",\n  \"claude-sonnet-4\",\n  \"claude-haiku-4\",\n  \"gpt-oss\",\n  \"gemini-2.5-pro\",\n  \"gemini-2.5-flash\",\n  \"gemini-2.0-flash-thinking\",\n  \"qwq\",\n  \"qwen3\",\n];\n\n/**\n * Strip a leading provider segment that Bedrock (and similar) prepend before\n * the actual family token — e.g. `anthropic.`, `us.anthropic.`, `eu.`. Keeps\n * everything from the recognised family token onward so prefix matching works.\n */\nfunction stripProviderPrefix(id: string): string {\n  // Remove leading region/provider segments (dot-separated) until we hit the\n  // model token. Provider/region segments never contain a hyphen, whereas\n  // model families do (e.g. `claude-3-5-sonnet`, `gpt-4.1`). So drop any\n  // leading dot-separated segment that has no hyphen.\n  let rest = id;\n  while (true) {\n    const dot = rest.indexOf(\".\");\n    if (dot === -1) break;\n    const head = rest.slice(0, dot);\n    if (head.length === 0 || head.includes(\"-\")) break;\n    rest = rest.slice(dot + 1);\n  }\n  return rest;\n}\n\n// Normalize each comma-separated env entry IDENTICALLY to the incoming model id\n// (date-suffix + provider/region prefix stripped, lowercased) so a prefixed\n// entry such as `anthropic.claude-opus-4` matches the already-stripped id.\nfunction parseEnvList(raw: string | undefined): string[] {\n  if (!raw) return [];\n  return raw\n    .split(\",\")\n    .map((s) => stripProviderPrefix(normalizeModelName(s.trim())?.toLowerCase() ?? \"\"))\n    .filter((s) => s.length > 0);\n}\n\nfunction matchesAny(id: string, families: readonly string[]): boolean {\n  return families.some((fam) => id.startsWith(fam));\n}\n\n/**\n * Returns whether the real provider for `model` would emit a reasoning/thinking\n * channel. Absence of a model id, or an unrecognised id, defaults to `true`\n * (assume capable) — see the policy note above.\n */\nexport function isReasoningModel(model: string | undefined): boolean {\n  if (!model) return true;\n\n  const normalized = normalizeModelName(model)?.toLowerCase() ?? \"\";\n  const id = stripProviderPrefix(normalized);\n\n  // Env overrides win, parsed fresh so they remain test-stubbable.\n  const envNon = parseEnvList(process.env.AIMOCK_NONREASONING_MODELS);\n  if (matchesAny(id, envNon)) return false;\n  const envReasoning = parseEnvList(process.env.AIMOCK_REASONING_MODELS);\n  if (matchesAny(id, envReasoning)) return true;\n\n  // Built-in denylist is the active guard.\n  if (matchesAny(id, NONREASONING_FAMILIES)) return false;\n  // Built-in reasoning list short-circuits to capable.\n  if (matchesAny(id, REASONING_FAMILIES)) return true;\n\n  // Unknown → assume reasoning-capable (fail open).\n  return true;\n}\n"],"mappings":";;AAAA,MAAM,iBAAiB;AAEvB,SAAgB,mBACd,OACA,mBACoB;AACpB,KAAI,CAAC,SAAS,kBAAmB,QAAO;AACxC,QAAO,MAAM,QAAQ,gBAAgB,GAAG;;AAoC1C,MAAM,wBAAwB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAGD,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;AAOD,SAAS,oBAAoB,IAAoB;CAK/C,IAAI,OAAO;AACX,QAAO,MAAM;EACX,MAAM,MAAM,KAAK,QAAQ,IAAI;AAC7B,MAAI,QAAQ,GAAI;EAChB,MAAM,OAAO,KAAK,MAAM,GAAG,IAAI;AAC/B,MAAI,KAAK,WAAW,KAAK,KAAK,SAAS,IAAI,CAAE;AAC7C,SAAO,KAAK,MAAM,MAAM,EAAE;;AAE5B,QAAO;;AAMT,SAAS,aAAa,KAAmC;AACvD,KAAI,CAAC,IAAK,QAAO,EAAE;AACnB,QAAO,IACJ,MAAM,IAAI,CACV,KAAK,MAAM,oBAAoB,mBAAmB,EAAE,MAAM,CAAC,EAAE,aAAa,IAAI,GAAG,CAAC,CAClF,QAAQ,MAAM,EAAE,SAAS,EAAE;;AAGhC,SAAS,WAAW,IAAY,UAAsC;AACpE,QAAO,SAAS,MAAM,QAAQ,GAAG,WAAW,IAAI,CAAC;;;;;;;AAQnD,SAAgB,iBAAiB,OAAoC;AACnE,KAAI,CAAC,MAAO,QAAO;CAGnB,MAAM,KAAK,oBADQ,mBAAmB,MAAM,EAAE,aAAa,IAAI,GACrB;AAI1C,KAAI,WAAW,IADA,aAAa,QAAQ,IAAI,2BAA2B,CACzC,CAAE,QAAO;AAEnC,KAAI,WAAW,IADM,aAAa,QAAQ,IAAI,wBAAwB,CACtC,CAAE,QAAO;AAGzC,KAAI,WAAW,IAAI,sBAAsB,CAAE,QAAO;AAElD,KAAI,WAAW,IAAI,mBAAmB,CAAE,QAAO;AAG/C,QAAO"}