janhq/jan · error · Error

Model with ID ${model.id} already exists

Error message

Model with ID ${model.id} already exists

What it means

Thrown by update() (model rename) when the destination folder <provider>/models/<model.id> already exists on disk. The method reads the source model.yml, computes the new folder path from model.id, and refuses to overwrite an existing model directory to prevent a destructive rename collision. fs.mv would otherwise silently swallow the source.

Source

Thrown at extensions/llamacpp-extension/src/index.ts:3023

   * @param model
   */
  async update(modelId: string, model: Partial<modelInfo>): Promise<void> {
    const modelFolderPath = await joinPath([
      await this.getProviderPath(),
      'models',
      modelId,
    ])
    const modelConfig = await invoke<ModelConfig>('read_yaml', {
      path: await joinPath([modelFolderPath, 'model.yml']),
    })
    const newFolderPath = await joinPath([
      await this.getProviderPath(),
      'models',
      model.id,
    ])
    // Check if newFolderPath exists
    if (await fs.existsSync(newFolderPath)) {
      throw new Error(`Model with ID ${model.id} already exists`)
    }
    const newModelConfigPath = await joinPath([newFolderPath, 'model.yml'])
    await fs.mv(modelFolderPath, newFolderPath).then(() =>
      // now replace what values have previous model name with format
      invoke('write_yaml', {
        data: {
          ...modelConfig,
          model_path: modelConfig?.model_path?.replace(
            `${this.providerId}/models/${modelId}`,
            `${this.providerId}/models/${model.id}`
          ),
          mmproj_path: modelConfig?.mmproj_path?.replace(
            `${this.providerId}/models/${modelId}`,
            `${this.providerId}/models/${model.id}`
          ),
        },
        savePath: newModelConfigPath,
      })

View on GitHub (pinned to fad3f12a14)

Solutions

  1. Choose a new model.id that does not collide - check fs.existsSync of the target folder first.
  2. If the destination is stale, delete that model (delete(model.id)) before renaming.
  3. Sanitize model.id (trim whitespace, normalize case) before calling update to avoid accidental collisions.
  4. If a previous rename crashed mid-way, manually clean up the leftover destination folder under <provider>/models/.

Example fix

// before
await provider.update('my-model', { id: 'qwen' }) // qwen already exists
// after
const dest = await joinPath([await provider.getProviderPath(), 'models', 'qwen'])
if (await fs.existsSync(dest)) await provider.delete('qwen')
await provider.update('my-model', { id: 'qwen' })
Defensive patterns

Strategy: validation

Validate before calling

// Ensure destination is free before renaming
const dest = await joinPath([await provider.getProviderPath(), 'models', model.id])
if (await fs.existsSync(dest)) {
  throw new Error(`Refusing rename: destination '${model.id}' already exists`)
}

Type guard

async function isModelIdFree(provider: { getProviderPath(): Promise<string> }, id: string): Promise<boolean> {
  const dir = await joinPath([await provider.getProviderPath(), 'models', id])
  return !(await fs.existsSync(dir))
}

Try / catch

try { await provider.update(oldId, { id: newId }) }
catch (e) {
  if (/already exists/.test(String(e))) { await provider.delete(newId); await provider.update(oldId, { id: newId }) }
  else throw e
}

Prevention

When it happens

Trigger: Calling update(oldId, { id: 'newId' }) when a model named 'newId' is already imported. model.id contains trailing whitespace or different casing that resolves to an existing folder on a case-insensitive filesystem. A previous rename partially completed leaving the destination folder behind.

Common situations: User tries to rename a model to a name they already used before and deleted incompletely. Import-and-rename workflow where the target id collides with a sibling model. Case-insensitive OS (Windows/macOS) collisions: renaming to a differently-cased id of an existing folder.

Related errors


AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12). Data as JSON: /api/errors/4544e955891a2e3e. Report an issue: GitHub.