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.
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.
This plugin implements a two-phase discovery strategy:
-
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. -
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.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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.
- Clone or download this repository
- Add to your
opencode.jsonconfig 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
}
}
}
}
}npm install opencode-persistent-model-discoveryThen in opencode.json:
{
"plugin": [
"opencode-persistent-model-discovery"
]
}{
"plugin": [
"github:lprzychodzien/opencode-persistent-model-discovery"
]
}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/v1for OpenAI-compatible providers. The plugin requests models from{baseURL}/models.modelsDiscovery.enabled(required): Must betruefor this plugin to process the provider.apiKey(optional): Explicit API key. If omitted, plugin reads from OpenCode's auth store.
Store your API key in OpenCode's auth store (recommended):
opencode auth add surplus
opencode auth add openrouter
# Enter your API key when promptedOr set it in your config (less secure):
{
"provider": {
"surplus": {
"options": {
"apiKey": "your-api-key-here"
}
},
"openrouter": {
"options": {
"apiKey": "your-api-key-here"
}
}
}
}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 openrouterExpected 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
opencodeModels are loaded from cache at startup. The plugin will also refresh the cache in the background.
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
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 surplusWhat it does:
- Reads
opencode.jsonto find the provider's config - Reads
~/.local/share/opencode/auth.jsonfor the API key - Fetches
{baseURL}/modelsfrom the provider API - Saves results to
~/.cache/opencode/models-{provider}.json - Does NOT modify
opencode.json
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 surplusWhat it does:
- Reads
opencode.json - Empties
provider.{provider}.modelssection - Sets
modelsDiscovery.enabled: true - Saves config
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)
{
"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
Manual update:
node sync-models.mjs surplusAutomated update (cron):
# Run daily at 9 AM
0 9 * * * cd /path/to/plugin && node sync-models.mjs surplusShell alias:
# Add to .bashrc or .zshrc
alias sync-models='node /path/to/opencode-persistent-model-discovery/sync-models.mjs surplus'rm ~/.cache/opencode/models-*.jsonNext startup will run without cached models (Phase 2 will repopulate).
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 |
Cause: Cache is stale or missing. Fix:
node sync-models.mjs surplusCause: Auth not configured. Fix:
opencode auth add surplus
# Or add to config: "apiKey": "your-key"Cause: Incorrect baseURL or endpoint.
Fix: Verify baseURL includes /v1:
"baseURL": "https://api.example.com/api/inference/v1"Cause: Plugin loaded too late in OpenCode lifecycle.
Fix: Ensure plugin is listed first in opencode.json plugins array.
Checklist:
- Provider has
modelsDiscovery.enabled: true - Provider has
baseURLconfigured - API key is accessible (auth store or config)
- Plugin is listed in
opencode.jsonplugins array - Plugin path is correct
The plugin resolves API keys in this priority order:
provider.options.apiKeyinopencode.json- OpenCode auth store:
~/.local/share/opencode/auth.json(entries oftype: "api")
Recommendation: Use the auth store for security. Never commit API keys to version control.
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.
| 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 |
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
# 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- Fork the repository
- Create a feature branch
- Make changes
- Test with
sync-models.mjs <provider> - Submit a pull request
- OpenCode: >= 1.2.0
- Node.js: >= 18.0.0
- Provider: OpenAI-compatible with
/v1/modelsendpoint
MIT License - see LICENSE file for details.
- opencode - The AI editor that this plugin extends
- opencode-models-discovery - The built-in discovery plugin that inspired this approach
- Issues: GitHub Issues
- Discussions: GitHub Discussions
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.