oc-blackbytes 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -6
  2. package/dist/index.js +204 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -62,7 +62,7 @@ opencode debug config
62
62
 
63
63
  ## Configuration
64
64
 
65
- Create `oc-blackbytes.jsonc` in the OpenCode config directory.
65
+ Create `oc-blackbytes.jsonc` in the OpenCode config directory. For the full configuration guide with recommended models per agent and example setups, see [docs/configuration.md](docs/configuration.md).
66
66
 
67
67
  ```jsonc
68
68
  {
@@ -97,11 +97,11 @@ Create `oc-blackbytes.jsonc` in the OpenCode config directory.
97
97
  | `disabled_tools` | `string[]` | `[]` | Prevents bundled tools from being registered. |
98
98
  | `mcp_env_alllowlist` | `string[]` | `[]` | Recognized by the schema for MCP environment filtering workflows. |
99
99
  | `hashline_edit` | `boolean` | `true` | Enables the `hashline_edit` tool and `tool.execute.after` hashline post-processing for `read`/`write`. |
100
- | `model_fallback` | `boolean` | `false` | Recognized by the schema for model compatibility workflows. |
100
+ | `model_fallback` | `boolean` | `false` | Enables model fallback resolution: discovers connected providers at init and resolves fallback chains when a preferred model's provider is unavailable. |
101
101
  | `auto_update` | `boolean` | `false` | Recognized by the schema for maintenance workflows. |
102
102
  | `websearch.provider` | `"exa" \| "tavily"` | `"exa"` | Selects the built-in `websearch` MCP backend. |
103
103
  | `agents` | `Record<string, AgentModelConfig>` | `{}` | Per-agent model configuration overrides. See [Per-agent model configuration](#per-agent-model-configuration). |
104
- | `fallback_models` | `string \| (string \| FallbackModelObject)[]` | — | Global fallback model chain (reserved for future use). |
104
+ | `fallback_models` | `string \| (string \| FallbackModelObject)[]` | — | Global fallback model chain. When `model_fallback: true` and an agent's primary model is unavailable, the plugin walks this chain and uses the first model whose provider is connected. |
105
105
  | `_migrations` | `string[]` | `[]` | Internal migration bookkeeping. |
106
106
 
107
107
  ## Built-in agents
@@ -150,9 +150,9 @@ Each agent model config supports:
150
150
  | `model` | `string` | Model identifier (e.g., `"openai/gpt-5.4"`). Drives prompt variant selection and, for subagents, sets the model hint. |
151
151
  | `reasoningEffort` | `string` | Override reasoning effort level for OpenAI reasoning models (`"low"`, `"medium"`, `"high"`). |
152
152
  | `temperature` | `number` | Override temperature for the agent. |
153
- | `fallback_models` | `string \| (string \| object)[]` | Per-agent fallback chain (reserved for future use). |
153
+ | `fallback_models` | `string \| (string \| object)[]` | Per-agent fallback chain tried before the global `fallback_models` when the primary model's provider is unavailable. Requires `model_fallback: true`. |
154
154
 
155
- When a `model` is specified for a subagent, the factory selects the appropriate prompt variant for that model family (Claude, GPT, or Gemini). The primary agent (`bytes`) uses the model hint for prompt selection only — the actual model is determined by the OpenCode UI selection.
155
+ When a `model` is specified for a subagent, the factory selects the appropriate prompt variant for that model family (Claude, GPT, or Gemini). The primary agent (`bytes`) uses the model hint for prompt selection only — the actual model is determined by the OpenCode UI selection. For recommended models per agent, see [docs/configuration.md](docs/configuration.md).
156
156
 
157
157
  ## Runtime model parameter adaptation
158
158
 
@@ -307,9 +307,10 @@ oc-blackbytes/
307
307
  │ ├── shared/ # Logger, constants, config path resolution, JSONC utils
308
308
  │ ├── compat/ # Compatibility integrations
309
309
  │ ├── integrations/ # Additional runtime integrations
310
- │ ├── services/ # Reserved service modules
310
+ │ ├── services/ # Model fallback resolution (provider discovery, fallback chains)
311
311
  │ └── stores/ # Reserved state stores
312
312
  ├── docs/
313
+ │ ├── configuration.md
313
314
  │ └── debugging.md
314
315
  ├── test/
315
316
  │ └── config.test.ts
package/dist/index.js CHANGED
@@ -2287,6 +2287,201 @@ function createOracleAgent(model2) {
2287
2287
  }
2288
2288
  createOracleAgent.mode = MODE5;
2289
2289
 
2290
+ // src/services/model-requirements.ts
2291
+ var BUILTIN_FALLBACK_CHAINS = {
2292
+ oracle: [
2293
+ { model: "gpt-5.4", providers: ["openai", "github-copilot"], reasoningEffort: "high" },
2294
+ { model: "gemini-3.1-pro", providers: ["google"] },
2295
+ { model: "claude-opus-4-6", providers: ["anthropic", "github-copilot"] }
2296
+ ],
2297
+ explore: [
2298
+ { model: "gemini-3-flash", providers: ["google"], temperature: 0.1 },
2299
+ { model: "claude-haiku-4-5", providers: ["anthropic", "github-copilot"], temperature: 0.1 },
2300
+ { model: "gpt-5-nano", providers: ["openai", "github-copilot"], temperature: 0.1 },
2301
+ { model: "minimax-m2.7", providers: ["minimax"], temperature: 0.1 }
2302
+ ],
2303
+ librarian: [
2304
+ { model: "claude-haiku-4-5", providers: ["anthropic", "github-copilot"], temperature: 0.2 },
2305
+ { model: "gemini-3-flash", providers: ["google"], temperature: 0.2 },
2306
+ { model: "gpt-5-nano", providers: ["openai", "github-copilot"], temperature: 0.2 }
2307
+ ],
2308
+ general: [
2309
+ { model: "claude-sonnet-4-6", providers: ["anthropic", "github-copilot"] },
2310
+ { model: "kimi-k2.5", providers: ["kimi"] },
2311
+ { model: "gpt-5.4-mini", providers: ["openai", "github-copilot"] },
2312
+ { model: "gemini-3.1-pro", providers: ["google"] }
2313
+ ]
2314
+ };
2315
+ // src/services/model-resolver.ts
2316
+ async function discoverAvailableModels(client) {
2317
+ try {
2318
+ const result = await client.provider.list();
2319
+ const data = result.data;
2320
+ if (!data) {
2321
+ log("[model-resolver] Provider list returned no data, skipping fallback resolution");
2322
+ return new Map;
2323
+ }
2324
+ const connected = new Set(data.connected);
2325
+ const models = new Map;
2326
+ for (const provider of data.all ?? []) {
2327
+ if (!connected.has(provider.id))
2328
+ continue;
2329
+ const modelIds = new Set(Object.keys(provider.models ?? {}));
2330
+ if (modelIds.size > 0) {
2331
+ models.set(provider.id, modelIds);
2332
+ }
2333
+ }
2334
+ const providerSummary = [...models.entries()].map(([id, m]) => `${id}(${m.size})`).join(", ");
2335
+ log(`[model-resolver] Available: ${providerSummary || "(none)"}`);
2336
+ return models;
2337
+ } catch (e) {
2338
+ log(`[model-resolver] Failed to discover providers: ${e}`);
2339
+ return new Map;
2340
+ }
2341
+ }
2342
+ function resolveModelRef(modelRef, availableModels) {
2343
+ if (availableModels.size === 0)
2344
+ return modelRef;
2345
+ const slashIdx = modelRef.indexOf("/");
2346
+ if (slashIdx === -1)
2347
+ return modelRef;
2348
+ const providerId = modelRef.substring(0, slashIdx);
2349
+ const modelId = modelRef.substring(slashIdx + 1);
2350
+ const providerModels = availableModels.get(providerId);
2351
+ if (!providerModels)
2352
+ return;
2353
+ const matchedModel = prefixMatchModel(modelId, providerModels);
2354
+ return matchedModel ? `${providerId}/${matchedModel}` : undefined;
2355
+ }
2356
+ function prefixMatchModel(modelPrefix, providerModels) {
2357
+ if (providerModels.has(modelPrefix))
2358
+ return modelPrefix;
2359
+ let bestMatch;
2360
+ for (const available of providerModels) {
2361
+ if (available.startsWith(modelPrefix)) {
2362
+ const rest = available.substring(modelPrefix.length);
2363
+ if (rest === "" || /^-\d/.test(rest)) {
2364
+ if (!bestMatch || available.length < bestMatch.length) {
2365
+ bestMatch = available;
2366
+ }
2367
+ }
2368
+ }
2369
+ }
2370
+ return bestMatch;
2371
+ }
2372
+ function resolveChainEntry(entry, availableModels) {
2373
+ for (const provider of entry.providers) {
2374
+ const providerModels = availableModels.get(provider);
2375
+ if (!providerModels)
2376
+ continue;
2377
+ const matched = prefixMatchModel(entry.model, providerModels);
2378
+ if (matched) {
2379
+ return {
2380
+ model: `${provider}/${matched}`,
2381
+ fromFallback: true,
2382
+ reasoningEffort: entry.reasoningEffort,
2383
+ temperature: entry.temperature
2384
+ };
2385
+ }
2386
+ }
2387
+ return;
2388
+ }
2389
+ function walkFallbackChain(fallbacks, availableModels) {
2390
+ if (!fallbacks)
2391
+ return;
2392
+ const chain = typeof fallbacks === "string" ? [fallbacks] : fallbacks;
2393
+ for (const entry of chain) {
2394
+ const modelRef = typeof entry === "string" ? entry : entry.model;
2395
+ const resolvedModel = resolveModelRef(modelRef, availableModels);
2396
+ if (resolvedModel) {
2397
+ return typeof entry === "string" ? { model: resolvedModel, fromFallback: true } : {
2398
+ model: resolvedModel,
2399
+ fromFallback: true,
2400
+ reasoningEffort: entry.reasoningEffort,
2401
+ temperature: entry.temperature
2402
+ };
2403
+ }
2404
+ }
2405
+ return;
2406
+ }
2407
+ function walkBuiltinChain(agentName, availableModels) {
2408
+ const chain = BUILTIN_FALLBACK_CHAINS[agentName];
2409
+ if (!chain)
2410
+ return;
2411
+ for (const entry of chain) {
2412
+ const resolved = resolveChainEntry(entry, availableModels);
2413
+ if (resolved)
2414
+ return resolved;
2415
+ }
2416
+ return;
2417
+ }
2418
+ function resolveAgentModel(agentName, agentConfig, globalFallbacks, availableModels) {
2419
+ const primaryModel = agentConfig?.model;
2420
+ if (!primaryModel) {
2421
+ const perAgentResolved2 = walkFallbackChain(agentConfig?.fallback_models, availableModels);
2422
+ if (perAgentResolved2) {
2423
+ log(` [model-resolver] ${agentName}: resolved \u2192 ${perAgentResolved2.model} (agent fallback)`);
2424
+ return perAgentResolved2;
2425
+ }
2426
+ const builtinResolved2 = walkBuiltinChain(agentName, availableModels);
2427
+ if (builtinResolved2) {
2428
+ log(` [model-resolver] ${agentName}: resolved \u2192 ${builtinResolved2.model} (builtin chain)`);
2429
+ return builtinResolved2;
2430
+ }
2431
+ const globalResolved2 = walkFallbackChain(globalFallbacks, availableModels);
2432
+ if (globalResolved2) {
2433
+ log(` [model-resolver] ${agentName}: resolved \u2192 ${globalResolved2.model} (global fallback)`);
2434
+ return globalResolved2;
2435
+ }
2436
+ return { model: "", fromFallback: false };
2437
+ }
2438
+ const resolvedPrimaryModel = resolveModelRef(primaryModel, availableModels);
2439
+ if (resolvedPrimaryModel) {
2440
+ log(` [model-resolver] ${agentName}: using primary model ${resolvedPrimaryModel}`);
2441
+ return { model: resolvedPrimaryModel, fromFallback: false };
2442
+ }
2443
+ log(` [model-resolver] ${agentName}: primary model ${primaryModel} not available, trying fallbacks...`);
2444
+ const perAgentResolved = walkFallbackChain(agentConfig?.fallback_models, availableModels);
2445
+ if (perAgentResolved) {
2446
+ log(` [model-resolver] ${agentName}: resolved \u2192 ${perAgentResolved.model} (agent fallback)`);
2447
+ return perAgentResolved;
2448
+ }
2449
+ const builtinResolved = walkBuiltinChain(agentName, availableModels);
2450
+ if (builtinResolved) {
2451
+ log(` [model-resolver] ${agentName}: resolved \u2192 ${builtinResolved.model} (builtin chain)`);
2452
+ return builtinResolved;
2453
+ }
2454
+ const globalResolved = walkFallbackChain(globalFallbacks, availableModels);
2455
+ if (globalResolved) {
2456
+ log(` [model-resolver] ${agentName}: resolved \u2192 ${globalResolved.model} (global fallback)`);
2457
+ return globalResolved;
2458
+ }
2459
+ log(` [model-resolver] ${agentName}: no available fallback, using OpenCode default`);
2460
+ return { model: "", fromFallback: false };
2461
+ }
2462
+ function resolveAllAgentModels(pluginConfig, availableModels) {
2463
+ const agentOverrides = pluginConfig.agents;
2464
+ const globalFallbacks = pluginConfig.fallback_models;
2465
+ const agentNames = new Set([
2466
+ ...Object.keys(BUILTIN_FALLBACK_CHAINS),
2467
+ ...agentOverrides ? Object.keys(agentOverrides) : []
2468
+ ]);
2469
+ if (agentNames.size === 0)
2470
+ return;
2471
+ const resolved = {};
2472
+ log("[model-resolver] Resolving agent models with fallback chains...");
2473
+ for (const name of agentNames) {
2474
+ const config = agentOverrides?.[name];
2475
+ const resolution = resolveAgentModel(name, config, globalFallbacks, availableModels);
2476
+ resolved[name] = {
2477
+ ...config,
2478
+ model: resolution.model || undefined,
2479
+ ...resolution.fromFallback && resolution.reasoningEffort !== undefined && config?.reasoningEffort === undefined ? { reasoningEffort: resolution.reasoningEffort } : {},
2480
+ ...resolution.fromFallback && resolution.temperature !== undefined && config?.temperature === undefined ? { temperature: resolution.temperature } : {}
2481
+ };
2482
+ }
2483
+ return resolved;
2484
+ }
2290
2485
  // src/handlers/config-handler/agent-config-handler.ts
2291
2486
  var DEFAULT_AGENT_NAME = "bytes";
2292
2487
  var SUPERSEDED_AGENTS = ["build", "plan"];
@@ -2363,7 +2558,8 @@ function handleAgentConfig(ctx) {
2363
2558
  const { disabled_agents: pluginDisabledAgents = [] } = ctx.pluginConfig;
2364
2559
  const userAgents = ctx.config.agent;
2365
2560
  const userDisabledAgentNames = captureUserDisabledAgents(userAgents);
2366
- const builtinAgents = createBuiltinAgents(ctx.pluginConfig.agents);
2561
+ const effectiveOverrides = ctx.availableModels.size > 0 ? resolveAllAgentModels(ctx.pluginConfig, ctx.availableModels) ?? ctx.pluginConfig.agents : ctx.pluginConfig.agents;
2562
+ const builtinAgents = createBuiltinAgents(effectiveOverrides);
2367
2563
  const merged = {
2368
2564
  ...builtinAgents
2369
2565
  };
@@ -2511,10 +2707,10 @@ function handleMcpConfig(ctx) {
2511
2707
  }
2512
2708
 
2513
2709
  // src/handlers/config-handler/index.ts
2514
- function handleConfig(pluginConfig) {
2710
+ function handleConfig(pluginConfig, availableModels) {
2515
2711
  return {
2516
2712
  config: async (config) => {
2517
- const configCtx = { config, pluginConfig };
2713
+ const configCtx = { config, pluginConfig, availableModels };
2518
2714
  handleMcpConfig(configCtx);
2519
2715
  handleAgentConfig(configCtx);
2520
2716
  }
@@ -17634,10 +17830,11 @@ function handleTools(pluginConfig, ctx) {
17634
17830
  // src/bootstrap.ts
17635
17831
  async function createOpenCodePlugin({
17636
17832
  input,
17637
- pluginConfig
17833
+ pluginConfig,
17834
+ availableModels
17638
17835
  }) {
17639
17836
  return {
17640
- ...handleConfig(pluginConfig),
17837
+ ...handleConfig(pluginConfig, availableModels),
17641
17838
  ...handleChatHeaders(pluginConfig),
17642
17839
  ...handleChatParams(pluginConfig),
17643
17840
  tool: handleTools(pluginConfig, input),
@@ -31250,7 +31447,8 @@ function loadPluginConfig(_input) {
31250
31447
  // src/index.ts
31251
31448
  var BlackbytesPlugin = async (ctx) => {
31252
31449
  const pluginConfig = loadPluginConfig(ctx);
31253
- return createOpenCodePlugin({ input: ctx, pluginConfig });
31450
+ const availableModels = pluginConfig.model_fallback ? await discoverAvailableModels(ctx.client) : new Map;
31451
+ return createOpenCodePlugin({ input: ctx, pluginConfig, availableModels });
31254
31452
  };
31255
31453
  var src_default = BlackbytesPlugin;
31256
31454
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-blackbytes",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "An OpenCode plugin tailored to streamline my everyday workflow.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",