{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-gateway-models",
  "title": "useGatewayModels",
  "description": "Fetches a gateway model catalog and keeps the selection valid against it: explicit pick, then preferred default, then first available.",
  "files": [
    {
      "path": "src/hooks/use-gateway-models.ts",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\n\n/** Structurally compatible with ModelSelect's AiModel. */\nexport interface GatewayModel {\n  /** Gateway model id in short form, e.g. \"gpt-5-2\". */\n  id: string;\n  /** Human-readable name, e.g. \"GPT-5.2\". */\n  name: string;\n  /** Provider name used for grouping, e.g. \"OpenAI\". */\n  provider: string;\n  /** Whether the model supports extended reasoning. */\n  reasoning?: boolean;\n}\n\nexport type GatewayModelsStatus = \"loading\" | \"ready\" | \"error\";\n\nexport interface UseGatewayModelsOptions {\n  /**\n   * URL of the catalog endpoint. Point it at your server-side proxy in\n   * front of the gateway's `GET /v1/models` — the bearer token must not\n   * ship to the browser. Accepts either the raw OpenAI-compatible shape\n   * (`{ data: [...] }`, filtered to enabled models) or a pre-shaped\n   * `{ models: GatewayModel[] }` payload.\n   */\n  endpoint?: string;\n  /** Preferred model id; wins whenever the catalog includes it. */\n  defaultModel?: string;\n}\n\n/** Raw entry from the gateway's OpenAI-compatible `GET /v1/models`. */\ninterface RawGatewayModel {\n  id: string;\n  name?: string;\n  owned_by?: string;\n  enabled?: boolean;\n}\n\n/** Display names for `owned_by` slugs the gateway reports. */\nconst PROVIDER_NAMES: Record<string, string> = {\n  alibaba: \"Alibaba\",\n  anthropic: \"Anthropic\",\n  databricks: \"Databricks\",\n  google: \"Google\",\n  meta: \"Meta\",\n  \"meta-llama\": \"Meta\",\n  mistral: \"Mistral\",\n  openai: \"OpenAI\",\n  qwen: \"Qwen\",\n};\n\nconst shapeModels = (payload: unknown): GatewayModel[] => {\n  if (typeof payload !== \"object\" || payload === null) {\n    return [];\n  }\n\n  // Pre-shaped proxy response: { models: GatewayModel[] }.\n  if (\"models\" in payload && Array.isArray(payload.models)) {\n    return payload.models as GatewayModel[];\n  }\n\n  // OpenAI-compatible response: { data: [...] }, keep enabled models only.\n  if (\"data\" in payload && Array.isArray(payload.data)) {\n    return (payload.data as RawGatewayModel[])\n      .filter((model) => model.enabled !== false)\n      .map((model) => ({\n        id: model.id,\n        name: model.name || model.id,\n        provider:\n          PROVIDER_NAMES[model.owned_by ?? \"\"] ?? model.owned_by ?? \"Other\",\n      }));\n  }\n\n  return [];\n};\n\n/**\n * The live model catalog of a Neon AI Gateway branch, with a selection\n * that is always valid against it: an explicit pick wins while listed,\n * then `defaultModel` when listed, then the first available model.\n * Hardcoded ids the gateway has not enabled can never be submitted.\n */\nexport const useGatewayModels = ({\n  defaultModel,\n  endpoint = \"/api/models\",\n}: UseGatewayModelsOptions = {}): {\n  /** The selected, catalog-validated model id; undefined while empty. */\n  model: string | undefined;\n  /** Enabled models on this gateway branch, shaped for ModelSelect. */\n  models: GatewayModel[];\n  setModel: (id: string) => void;\n  status: GatewayModelsStatus;\n} => {\n  const [models, setModels] = useState<GatewayModel[]>([]);\n  const [status, setStatus] = useState<GatewayModelsStatus>(\"loading\");\n  const [picked, setPicked] = useState<string>();\n\n  useEffect(() => {\n    const controller = new AbortController();\n\n    (async () => {\n      try {\n        const response = await fetch(endpoint, { signal: controller.signal });\n\n        if (!response.ok) {\n          setStatus(\"error\");\n          return;\n        }\n\n        setModels(shapeModels(await response.json()));\n        setStatus(\"ready\");\n      } catch {\n        if (!controller.signal.aborted) {\n          setStatus(\"error\");\n        }\n      }\n    })();\n\n    return () => controller.abort();\n  }, [endpoint]);\n\n  // Derived, never snapped: the selection re-validates on every render,\n  // so a stale pick heals itself if the catalog changes underneath it.\n  const model = useMemo(() => {\n    const listed = (id: string | undefined) =>\n      id !== undefined && models.some((entry) => entry.id === id);\n\n    if (listed(picked)) {\n      return picked;\n    }\n    if (listed(defaultModel)) {\n      return defaultModel;\n    }\n    return models[0]?.id;\n  }, [models, picked, defaultModel]);\n\n  return { model, models, setModel: setPicked, status };\n};\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}