import type OpenAI from "openai"; import { type PolyModel, modelKey } from "@/lib/ai/core/types"; import { createOpenAICompatAdapter, listOpenAIModels } from "../shared/openai-compat/factory"; import { CEREBRAS_CATALOG } from "./catalog"; /** * Cerebras Inference — OpenAI-compatible at https://api.cerebras.ai/v1 (open models, very fast). * `GET /v1/models` exposes ids only; the catalog (docs/provider-research/cerebras.md) provides * limits, pricing and which reasoning knob each model accepts. */ export function normalizeCerebrasModel(m: { id: string }): PolyModel | null { const cat = CEREBRAS_CATALOG.get(m.id); return { key: modelKey("cerebras", m.id), id: m.id, provider: "cerebras", displayName: cat?.displayName ?? prettify(m.id), family: cat?.family ?? (m.id.startsWith("gpt-oss") ? "GPT-OSS" : m.id.startsWith("qwen") ? "Qwen" : m.id.startsWith("gemma") ? "Gemma" : m.id.startsWith("llama") ? "Llama" : "Open models"), capabilities: cat?.capabilities ?? { text: true, vision: !m.id.startsWith("gpt-oss"), audioInput: false, audioOutput: false, imageGeneration: false, video: false, reasoning: true, tools: true, structuredOutput: true, streaming: true, files: false, webSearch: false }, limits: cat?.limits ?? {}, parameters: cat?.parameters ?? { temperature: true, topP: true, maxTokens: true, stop: true, seed: true, frequencyPenalty: true, presencePenalty: true, topK: false, reasoningEffort: true, reasoningEffortLevels: ["low", "medium", "high"], temperatureRange: { min: 0, max: 2 } }, status: cat?.status ?? "active", pricing: cat?.pricing ?? null, metadata: { ...(cat?.metadata ?? {}), sortWeight: cat?.sortWeight ?? (m.id.startsWith("gpt-oss") ? 100 : m.id.startsWith("qwen") ? 90 : 80) }, }; } function prettify(id: string) { return id.replace(/gpt-oss/, "GPT-OSS").replace(/qwen/, "Qwen").replace(/gemma/, "Gemma").replace(/llama/, "Llama").replace(/-(\d+)b\b/, " $1B").replace(/-/g, " "); } async function list(client: OpenAI, _k: string, signal?: AbortSignal): Promise { return (await listOpenAIModels(client, signal)).map(normalizeCerebrasModel).filter((m): m is PolyModel => Boolean(m)); } export const cerebrasAdapter = createOpenAICompatAdapter({ id: "cerebras", name: "Cerebras", baseURL: "https://api.cerebras.ai/v1", keyDocsUrl: "https://cloud.cerebras.ai/", keyPrefixHint: "csk-", listModels: list, messageOptions: { inlineFiles: true }, tweakParams: (params, settings, req) => { const p = params as unknown as Record; const meta = (req.modelInfo?.metadata ?? {}) as Record; // Cerebras validates strictly: only `reasoning_effort` exists (no disable_reasoning / budget). if (req.modelInfo?.capabilities.reasoning && settings.reasoningEffort) { const levels = req.modelInfo.parameters.reasoningEffortLevels ?? ["low", "medium", "high"]; const wanted = settings.reasoningEffort === "minimal" ? "low" : settings.reasoningEffort === "xhigh" || settings.reasoningEffort === "max" ? "high" : settings.reasoningEffort; if (levels.includes(wanted)) p.reasoning_effort = wanted; } // gpt-oss rejects tools + response_format together — tools win, JSON is requested in the prompt instead. if (meta.toolsWithResponseFormat === false && params.tools?.length && params.response_format) delete p.response_format; // Reasoning tokens consume the output cap; never leave a reasoning model with a tiny cap. if (req.modelInfo?.capabilities.reasoning && typeof params.max_completion_tokens === "number" && params.max_completion_tokens < Number(meta.minOutputForReasoning ?? 1000)) params.max_completion_tokens = Number(meta.minOutputForReasoning ?? 1000); }, refineError: (status, code, message) => { if (status === 401) return "INVALID_API_KEY"; if (status === 404) return "MODEL_NOT_FOUND"; if (status === 429 || code === "token_quota_exceeded" || /quota/i.test(message)) return "RATE_LIMITED"; if (code === "wrong_api_key" || code === "invalid_api_key") return "INVALID_API_KEY"; if (code === "model_not_found") return "MODEL_NOT_FOUND"; if (code === "context_length_exceeded") return "CONTEXT_TOO_LONG"; return undefined; }, });