From 2c873336fcd2d7094c36af8edca3f0d9cc041ea1 Mon Sep 17 00:00:00 2001 From: dragon-Elec Date: Sat, 20 Jun 2026 19:05:00 +0530 Subject: [PATCH] Feat: Decouple Model Definitions using Host config Hook --- src/plugin.ts | 37 +++++++++++++++++++++ src/plugin/config/loader.ts | 65 +++++++++++++++++++++++++++++++++---- src/plugin/config/schema.ts | 5 +++ src/plugin/types.ts | 1 + 4 files changed, 102 insertions(+), 6 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index 29f5d586..90d4f446 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -59,6 +59,7 @@ import { initHealthTracker, getHealthTracker, initTokenTracker, getTokenTracker import { getAntigravityVersionResolution, initAntigravityVersion } from "./plugin/version"; import { executeSearch, initSearchSessionId } from "./plugin/search"; import { fetchWithRawTransport } from "./plugin/transport"; +import { OPENCODE_MODEL_DEFINITIONS } from "./plugin/model-registry"; import type { GetAuth, LoaderResult, @@ -1533,6 +1534,9 @@ export const createAntigravityPlugin = (providerId: string) => async ( }); return { + config: async (opencodeConfig: Record) => { + applyAntigravityProviderCatalog(opencodeConfig, providerId, config); + }, event: eventHandler, tool: { google_search: googleSearchTool, @@ -3925,6 +3929,39 @@ function isExplicitQuotaFromUrl(urlString: string): boolean { return explicitQuota ?? false; } +type OpencodeMutableConfig = Record & { + provider?: Record & { + models?: Record; + whitelist?: string[]; + }>; +}; + +function applyAntigravityProviderCatalog( + opencodeConfig: Record, + providerId: string, + pluginConfig: AntigravityConfig +): void { + const mutableConfig = opencodeConfig as OpencodeMutableConfig; + mutableConfig.provider ??= {}; + + const providerConfig = mutableConfig.provider[providerId] ?? {}; + + // Merge order (lowest to highest priority): + // 1. Built-in defaults: OPENCODE_MODEL_DEFINITIONS + // 2. Decoupled models (from antigravity.json / antigravity-models.json): pluginConfig.models + // 3. User's main opencode.json models (preserves backwards compatibility / custom overrides) + providerConfig.models = { + ...OPENCODE_MODEL_DEFINITIONS, + ...(pluginConfig.models ?? {}), + ...(providerConfig.models ?? {}), + }; + + // Whitelist should be the union of all registered models so they aren't pruned by OpenCode + providerConfig.whitelist = Object.keys(providerConfig.models); + + mutableConfig.provider[providerId] = providerConfig; +} + export const __testExports = { getHeaderStyleFromUrl, resolveHeaderRoutingDecision, diff --git a/src/plugin/config/loader.ts b/src/plugin/config/loader.ts index 985783bf..5e7859ce 100644 --- a/src/plugin/config/loader.ts +++ b/src/plugin/config/loader.ts @@ -97,12 +97,56 @@ function mergeConfigs( // Main Loader // ============================================================================= -/** - * Load the complete configuration. - * - * @param directory - The project directory (for project-level config) - * @returns Fully resolved configuration - */ +function stripJsonCommentsAndTrailingCommas(json: string): string { + return json + .replace( + /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, + (match: string, group: string | undefined) => (group ? "" : match) + ) + .replace(/,(\s*[}\]])/g, "$1"); +} + +function loadModelsFile(path: string): Record | null { + try { + if (!existsSync(path)) { + return null; + } + const content = readFileSync(path, "utf-8"); + const parsed = JSON.parse(stripJsonCommentsAndTrailingCommas(content)); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed; + } + return null; + } catch (error) { + log.warn("Failed to load decoupled models file", { path, error: String(error) }); + return null; + } +} + +export function loadDecoupledModels(directory: string): Record { + let mergedModels: Record = {}; + + const configDir = getConfigDir(); + const userJsonPath = join(configDir, "antigravity-models.json"); + const userJsoncPath = join(configDir, "antigravity-models.jsonc"); + const projectJsonPath = join(directory, ".opencode", "antigravity-models.json"); + const projectJsoncPath = join(directory, ".opencode", "antigravity-models.jsonc"); + + // User level (prefer jsonc if both exist) + const userModels = loadModelsFile(existsSync(userJsoncPath) ? userJsoncPath : userJsonPath); + if (userModels) { + mergedModels = { ...mergedModels, ...userModels }; + } + + // Project level (prefer jsonc if both exist) - overrides user level + const projectModels = loadModelsFile(existsSync(projectJsoncPath) ? projectJsoncPath : projectJsonPath); + if (projectModels) { + mergedModels = { ...mergedModels, ...projectModels }; + } + + return mergedModels; +} + export function loadConfig(directory: string): AntigravityConfig { // Start with defaults let config: AntigravityConfig = { ...DEFAULT_CONFIG }; @@ -121,6 +165,15 @@ export function loadConfig(directory: string): AntigravityConfig { config = mergeConfigs(config, projectConfig); } + // Load decoupled models from antigravity-models.json(c) and merge into config.models + const decoupledModels = loadDecoupledModels(directory); + if (Object.keys(decoupledModels).length > 0) { + config.models = { + ...(config.models ?? {}), + ...decoupledModels, + }; + } + log.info("Config loaded", { strategy: config.account_selection_strategy, scheduling: config.scheduling_mode, diff --git a/src/plugin/config/schema.ts b/src/plugin/config/schema.ts index 6ceb7ee0..e1fc52e4 100644 --- a/src/plugin/config/schema.ts +++ b/src/plugin/config/schema.ts @@ -554,6 +554,11 @@ export const AntigravityConfigSchema = z.object({ */ auto_update: z.boolean().default(true), + /** + * Decoupled model definitions to inject. + */ + models: z.record(z.string(), z.any()).optional(), + }); export type AntigravityConfig = z.infer; diff --git a/src/plugin/types.ts b/src/plugin/types.ts index 97105ad7..1a58d90d 100644 --- a/src/plugin/types.ts +++ b/src/plugin/types.ts @@ -97,6 +97,7 @@ export interface PluginResult { }; event?: (payload: PluginEventPayload) => void; tool?: Record; + config?: (config: Record) => Promise; } export interface RefreshParts {