( apiModel: z.infer<typeof DoModel>, pricing: ModelPricing | undefined, existing: ExistingModel | null, )
| 359 | // --------------------------------------------------------------------------- |
| 360 | |
| 361 | function mergeModel( |
| 362 | apiModel: z.infer<typeof DoModel>, |
| 363 | pricing: ModelPricing | undefined, |
| 364 | existing: ExistingModel | null, |
| 365 | ): MergedModel { |
| 366 | const rawInput = apiModel.modalities?.input ?? []; |
| 367 | const rawOutput = apiModel.modalities?.output ?? []; |
| 368 | const inputMods = filterInputModalities(rawInput.length > 0 ? rawInput : existing?.modalities?.input ?? ["text"]); |
| 369 | const outputMods = filterOutputModalities(rawOutput.length > 0 ? rawOutput : existing?.modalities?.output ?? ["text"]); |
| 370 | |
| 371 | const maxTokensSetting = apiModel.settings?.find((s) => s.name === "max_tokens"); |
| 372 | const maxTokens = maxTokensSetting?.max ?? existing?.limit?.output ?? 0; |
| 373 | |
| 374 | const rawContext = apiModel.context_window; |
| 375 | const contextWindow = |
| 376 | rawContext !== undefined |
| 377 | ? typeof rawContext === "string" |
| 378 | ? parseInt(rawContext, 10) |
| 379 | : rawContext |
| 380 | : (existing?.limit?.context ?? 0); |
| 381 | |
| 382 | const isDeprecated = apiModel.lifecycle_status === "end_of_life"; |
| 383 | |
| 384 | // Fields preserved from existing TOML (APIs don't provide these) |
| 385 | const family = existing?.family ?? inferFamily(apiModel.id, apiModel.name); |
| 386 | const knowledge = existing?.knowledge; |
| 387 | const openWeights = existing?.open_weights ?? false; |
| 388 | const interleaved = existing?.interleaved; |
| 389 | const attachment = existing?.attachment ?? inputMods.some((m) => m !== "text"); |
| 390 | |
| 391 | // reasoning: trust existing if set, else use API thinking flag as a hint |
| 392 | // (thinking flag is unreliable for non-LLM models so gate on output modality) |
| 393 | const isTextOutput = outputMods.includes("text") && !outputMods.includes("image") && !outputMods.includes("video"); |
| 394 | const reasoning = existing?.reasoning ?? (isTextOutput && (apiModel.thinking ?? false)); |
| 395 | |
| 396 | // tool_call: no API signal, preserve existing or default true for text models |
| 397 | const toolCall = existing?.tool_call ?? isTextOutput; |
| 398 | |
| 399 | // temperature: no API signal, preserve or default true |
| 400 | const temperature = existing?.temperature ?? true; |
| 401 | |
| 402 | // structured_output: no API signal, preserve only |
| 403 | const structuredOutput = existing?.structured_output; |
| 404 | |
| 405 | const releaseDate = existing?.release_date ?? apiModel.created_at?.slice(0, 10) ?? getTodayDate(); |
| 406 | |
| 407 | const merged: MergedModel = { |
| 408 | name: apiModel.name, |
| 409 | family, |
| 410 | attachment, |
| 411 | reasoning, |
| 412 | tool_call: toolCall, |
| 413 | temperature, |
| 414 | release_date: releaseDate, |
| 415 | last_updated: getTodayDate(), |
| 416 | open_weights: openWeights, |
| 417 | ...(structuredOutput !== undefined && { structured_output: structuredOutput }), |
| 418 | ...(knowledge && { knowledge }), |
no test coverage detected