Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

Β 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

OpenCode Persistent Model Discovery

An OpenCode plugin that discovers and persistently caches AI models from OpenAI-compatible providers. Solves the race condition where OpenCode's provider resolution runs before async model discovery completes.

The Problem

OpenCode has a built-in opencode-models-discovery plugin that fetches models from provider APIs. However, it runs asynchronously during the config hook. OpenCode's provider resolution (which validates that requested models exist) runs before the async discovery finishes. This causes:

ProviderModelNotFoundError: Model not found: surplus/claude-opus-4.8

Even though the model exists and the API is accessible.

The Solution

This plugin implements a two-phase discovery strategy:

  1. Phase 1 - Synchronous Cache Load: At startup, immediately loads previously discovered models from a local cache file (~/.cache/opencode/models-{provider}.json). These are merged into the provider's model map before OpenCode resolves providers.

  2. Phase 2 - Async Background Refresh: After startup, asynchronously fetches the latest models from the provider's API and updates the cache for the next session.

This means models are always available immediately, while staying up-to-date in the background.

Architecture

              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚              OpenCode Startup Lifecycle              β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                          β”‚
                                          β–Ό
                                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                β”‚   config() hook  β”‚
                                β”‚      fires       β”‚
                                β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                         β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚                          β”‚                          β”‚
              β–Ό                          β–Ό                          β–Ό
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚ opencode.jsonβ”‚           β”‚  Cache File  β”‚           β”‚ Provider API β”‚
     β”‚              β”‚           β”‚ ~/.cache/    β”‚           β”‚ {baseURL}/   β”‚
     β”‚ β€’ baseURL    β”‚           β”‚ opencode/    β”‚           β”‚   models     β”‚
     β”‚ β€’ apiKey     β”‚           β”‚models-*.json β”‚           β”‚              β”‚
     β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚                          β”‚                          β”‚
            β”‚  provider settings       β”‚  cached models           β”‚  fresh models
            β”‚  (baseURL, apiKey)       β”‚  (from last sync)        β”‚  (current)
            β”‚                          β”‚                          β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚                           β”‚
                         β–Ό                           β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
              β”‚   Phase 1: Cache     β”‚               β”‚
              β”‚       Load           β”‚               β”‚
              β”‚                      β”‚               β”‚
              β”‚  Read cache file     β”‚               β”‚
              β”‚  Merge into          β”‚               β”‚
              β”‚   p.models           β”‚               β”‚
              β”‚  (immediate)         β”‚               β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
                         β”‚                           β”‚
                         β–Ό                           β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
              β”‚   Phase 2: API       β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚      Refresh         β”‚
              β”‚                      β”‚
              β”‚  Fetch /v1/models    β”‚
              β”‚  Write to cache      │──────▢ cache file
              β”‚  Merge into          β”‚
              β”‚   p.models           β”‚
              β”‚  (current session)   β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  config() returns    β”‚
              β”‚  (models merged)     β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  Provider Resolution β”‚
              β”‚  (models ready βœ…)   β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Both phases run inside OpenCode's config hook. Phase 1 awaits the cache read so models are merged into provider.{name}.models before the hook returns, and Phase 2 awaits the API fetch plus cache write in the same hook. By the time OpenCode validates the requested model, both cached and freshly discovered entries are already in p.models, eliminating the race condition.

Installation

Method 1: Local Directory (Recommended for Development)

  1. Clone or download this repository
  2. Add to your opencode.json config file:
{
  "plugin": [
    "/path/to/opencode-persistent-model-discovery"
  ],
  "provider": {
    "surplus": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Surplus Intelligence",
      "options": {
        "baseURL": "https://www.surplusintelligence.ai/api/inference/v1",
        "modelsDiscovery": {
          "enabled": true
        }
      }
    },
    "openrouter": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "OpenRouter",
      "options": {
        "baseURL": "https://openrouter.ai/api/v1",
        "modelsDiscovery": {
          "enabled": true
        }
      }
    }
  }
}

Method 2: From npm (When Published)

npm install opencode-persistent-model-discovery

Then in opencode.json:

{
  "plugin": [
    "opencode-persistent-model-discovery"
  ]
}

Method 3: GitHub Direct

{
  "plugin": [
    "github:lprzychodzien/opencode-persistent-model-discovery"
  ]
}

Quick Start

Step 1: Configure Your Provider

Ensure your provider has modelsDiscovery.enabled: true:

{
  "provider": {
    "surplus": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Surplus Intelligence",
      "options": {
        "baseURL": "https://www.surplusintelligence.ai/api/inference/v1",
        "modelsDiscovery": {
          "enabled": true
        }
      }
    }
  }
}

Configuration Options:

  • baseURL (required): The provider's API base URL. Must include /v1 for OpenAI-compatible providers. The plugin requests models from {baseURL}/models.
  • modelsDiscovery.enabled (required): Must be true for this plugin to process the provider.
  • apiKey (optional): Explicit API key. If omitted, plugin reads from OpenCode's auth store.

Step 2: Set Up Authentication

Store your API key in OpenCode's auth store (recommended):

opencode auth add surplus
opencode auth add openrouter
# Enter your API key when prompted

Or set it in your config (less secure):

{
  "provider": {
    "surplus": {
      "options": {
        "apiKey": "your-api-key-here"
      }
    },
    "openrouter": {
      "options": {
        "apiKey": "your-api-key-here"
      }
    }
  }
}

Step 3: Initial Cache Population

Run the sync script to discover and cache models (once per provider):

cd /path/to/opencode-persistent-model-discovery
node sync-models.mjs surplus
node sync-models.mjs openrouter

Expected output:

πŸ” Model Sync: surplus
=====================
πŸ“‘ Fetching models from https://www.surplusintelligence.ai/api/inference/v1...
   URL: https://www.surplusintelligence.ai/api/inference/v1/models
βœ… Found 242 models
πŸ’Ύ Saved 242 models to cache
πŸ“ Cache file: ~/.cache/opencode/models-surplus.json
πŸ“ Note: Models are now cached and will be loaded at startup

Step 4: Start OpenCode

opencode

Models are loaded from cache at startup. The plugin will also refresh the cache in the background.

File Reference

Plugin File: src/index.ts

The main plugin that hooks into OpenCode's config phase.

Key Behaviors:

  • Loads cached models synchronously before provider resolution
  • Runs async discovery after startup to refresh cache
  • Handles timeouts (10s for API calls)
  • Reads API keys from auth store or config
  • Supports multiple providers simultaneously

Cache Location: ~/.cache/opencode/models-{providerName}.json

Sync Script: sync-models.mjs

Standalone script to manually refresh the model cache for a provider.

Use Cases:

  • First-time setup
  • After provider adds new models
  • When you see "Model not found" errors (cache is stale)
  • CI/CD pipelines to pre-populate cache

Command:

node sync-models.mjs <provider>
# e.g. node sync-models.mjs surplus

What it does:

  1. Reads opencode.json to find the provider's config
  2. Reads ~/.local/share/opencode/auth.json for the API key
  3. Fetches {baseURL}/models from the provider API
  4. Saves results to ~/.cache/opencode/models-{provider}.json
  5. Does NOT modify opencode.json

Clean Script: clean-config.mjs

Removes hardcoded models from a provider in your config and enables discovery mode.

Use Case: When migrating from manual model lists to cached discovery.

Command:

node clean-config.mjs <provider>
# e.g. node clean-config.mjs surplus

What it does:

  1. Reads opencode.json
  2. Empties provider.{provider}.models section
  3. Sets modelsDiscovery.enabled: true
  4. Saves config

Cache Management

Where is the Cache?

The plugin uses XDG_CACHE_HOME if set, otherwise it falls back to ~/.cache/opencode/ on all platforms:

$XDG_CACHE_HOME/opencode/models-{provider}.json   # if XDG_CACHE_HOME is set
~/.cache/opencode/models-{provider}.json           # default (macOS, Linux, Windows)

Cache Format

{
  "claude-opus-4.8": {
    "id": "claude-opus-4.8",
    "name": "Claude Opus 4.8"
  },
  "gpt-5.5": {
    "id": "gpt-5.5",
    "name": "GPT 5.5"
  }
}

Each entry has:

  • id: Model identifier (used in API requests)
  • name: Human-readable display name

Updating the Cache

Manual update:

node sync-models.mjs surplus

Automated update (cron):

# Run daily at 9 AM
0 9 * * * cd /path/to/plugin && node sync-models.mjs surplus

Shell alias:

# Add to .bashrc or .zshrc
alias sync-models='node /path/to/opencode-persistent-model-discovery/sync-models.mjs surplus'

Clearing the Cache

rm ~/.cache/opencode/models-*.json

Next startup will run without cached models (Phase 2 will repopulate).

Supported Providers

Any provider implementing the OpenAI /v1/models endpoint:

Provider Base URL Status
Surplus Intelligence https://www.surplusintelligence.ai/api/inference/v1 βœ… Tested
OpenRouter https://openrouter.ai/api/v1 βœ… Compatible
Ollama http://localhost:11434/v1 βœ… Compatible
LiteLLM Custom βœ… Compatible
Custom proxies Custom βœ… Compatible

Troubleshooting

"Model not found" Error

Cause: Cache is stale or missing. Fix:

node sync-models.mjs surplus

"No API key found"

Cause: Auth not configured. Fix:

opencode auth add surplus
# Or add to config: "apiKey": "your-key"

"Discovery failed: 404"

Cause: Incorrect baseURL or endpoint. Fix: Verify baseURL includes /v1:

"baseURL": "https://api.example.com/api/inference/v1"

"Config object is frozen"

Cause: Plugin loaded too late in OpenCode lifecycle. Fix: Ensure plugin is listed first in opencode.json plugins array.

Provider not being processed

Checklist:

  1. Provider has modelsDiscovery.enabled: true
  2. Provider has baseURL configured
  3. API key is accessible (auth store or config)
  4. Plugin is listed in opencode.json plugins array
  5. Plugin path is correct

API Key Resolution

The plugin resolves API keys in this priority order:

  1. provider.options.apiKey in opencode.json
  2. OpenCode auth store: ~/.local/share/opencode/auth.json (entries of type: "api")

Recommendation: Use the auth store for security. Never commit API keys to version control.

Multiple Providers

The plugin supports multiple providers simultaneously. Each provider gets its own cache file:

{
  "provider": {
    "surplus": {
      "options": {
        "baseURL": "https://www.surplusintelligence.ai/api/inference/v1",
        "modelsDiscovery": { "enabled": true }
      }
    },
    "openrouter": {
      "options": {
        "baseURL": "https://openrouter.ai/api/v1",
        "modelsDiscovery": { "enabled": true }
      }
    }
  }
}

This creates:

  • ~/.cache/opencode/models-surplus.json
  • ~/.cache/opencode/models-openrouter.json

Run node sync-models.mjs <provider> for each provider you configure.

Comparison: Built-in vs This Plugin

Feature Built-in Discovery Persistent Discovery
Timing Async (after resolution) Sync cache + async refresh
Persistence In-memory only Cache file on disk
Startup models Available after delay Available immediately
Config pollution None None (cache is separate)
Error handling Silent failures Logs to console
Multi-provider Yes Yes
Manual refresh Not available sync-*.mjs script

Development

Project Structure

opencode-persistent-model-discovery/
β”œβ”€β”€ src/
β”‚   └── index.ts              # Main plugin code
β”œβ”€β”€ package.json              # Package manifest
β”œβ”€β”€ sync-models.mjs           # Manual sync script
β”œβ”€β”€ clean-config.mjs          # Config cleanup script
└── README.md                 # This file

Testing

# 1. Clean config
node clean-config.mjs surplus

# 2. Sync models
node sync-models.mjs surplus

# 3. Verify cache
ls -la ~/.cache/opencode/models-surplus.json

# 4. Test with opencode
opencode run "test" -m surplus/claude-opus-4.8

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make changes
  4. Test with sync-models.mjs <provider>
  5. Submit a pull request

Requirements

  • OpenCode: >= 1.2.0
  • Node.js: >= 18.0.0
  • Provider: OpenAI-compatible with /v1/models endpoint

License

MIT License - see LICENSE file for details.

Related Projects

Support


Note for AI Agents: This plugin solves a specific race condition in OpenCode's plugin architecture. The key implementation detail is the two-phase approach: synchronous cache loading (Phase 1) before provider resolution, followed by asynchronous API refresh (Phase 2) after startup. The cache file format is a simple JSON map of model IDs to {id, name} objects.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages