fix: handle YAML and JSONC config formats in providers (#39)
This commit is contained in:
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "clawsuite",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "clawsuite",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.1.0",
|
||||
@@ -44,6 +44,7 @@
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-search": "^0.13.0",
|
||||
"xterm-addon-web-links": "^0.9.0",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^3.25.76",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
@@ -9267,6 +9268,21 @@
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-search": "^0.13.0",
|
||||
"xterm-addon-web-links": "^0.9.0",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^3.25.76",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { parse as parseYaml } from 'yaml'
|
||||
|
||||
type GatewayConfig = {
|
||||
auth?: {
|
||||
@@ -35,6 +36,74 @@ export function invalidateCache(): void {
|
||||
cacheTimestamp = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip JS-style comments and trailing commas from a JSON-ish string (JSONC).
|
||||
*/
|
||||
function stripJsonc(raw: string): string {
|
||||
// Remove block comments /* ... */
|
||||
let result = raw.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
// Remove line comments // ...
|
||||
result = result.replace(/\/\/[^\n\r]*/g, '')
|
||||
// Remove trailing commas before } or ]
|
||||
result = result.replace(/,(\s*[}\]])/g, '$1')
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse the Gateway config file, supporting JSON, JSONC, YAML, and YML.
|
||||
* Resolution order:
|
||||
* 1. OPENCLAW_CONFIG_PATH env var (if set)
|
||||
* 2. ~/.openclaw/openclaw.json
|
||||
* 3. ~/.openclaw/openclaw.yaml
|
||||
* 4. ~/.openclaw/openclaw.yml
|
||||
* Returns null if no config file is found or parsing fails.
|
||||
*/
|
||||
function readGatewayConfig(): GatewayConfig | null {
|
||||
const dir = path.join(os.homedir(), '.openclaw')
|
||||
|
||||
// Candidates in priority order
|
||||
const candidates: string[] = []
|
||||
|
||||
const envPath = process.env['OPENCLAW_CONFIG_PATH']
|
||||
if (envPath) {
|
||||
candidates.push(envPath)
|
||||
}
|
||||
|
||||
candidates.push(
|
||||
path.join(dir, 'openclaw.json'),
|
||||
path.join(dir, 'openclaw.yaml'),
|
||||
path.join(dir, 'openclaw.yml'),
|
||||
)
|
||||
|
||||
for (const filePath of candidates) {
|
||||
let raw: string
|
||||
try {
|
||||
raw = fs.readFileSync(filePath, 'utf8')
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException)?.code
|
||||
if (code === 'ENOENT') continue // try next candidate
|
||||
// Unexpected I/O error — surface in dev
|
||||
if (import.meta.env.DEV) console.error(`Failed to read config at ${filePath}:`, err)
|
||||
continue
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
try {
|
||||
if (ext === '.yaml' || ext === '.yml') {
|
||||
return parseYaml(raw) as GatewayConfig
|
||||
} else {
|
||||
// .json or unknown — treat as JSONC (strip comments + trailing commas)
|
||||
return JSON.parse(stripJsonc(raw)) as GatewayConfig
|
||||
}
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error(`Failed to parse config at ${filePath}:`, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract provider name from auth profile key.
|
||||
* Example: "anthropic:default" -> "anthropic"
|
||||
@@ -67,11 +136,9 @@ function modelIdFromScopedKey(scoped: string): string | null {
|
||||
export function getConfiguredProviderNames(): Array<string> {
|
||||
if (cachedProviderNames && !isCacheStale()) return cachedProviderNames
|
||||
|
||||
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json')
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(configPath, 'utf8')
|
||||
const config = JSON.parse(raw) as GatewayConfig
|
||||
const config = readGatewayConfig()
|
||||
if (!config) return []
|
||||
|
||||
const providerNames = new Set<string>()
|
||||
|
||||
@@ -94,12 +161,8 @@ export function getConfiguredProviderNames(): Array<string> {
|
||||
cacheTimestamp = Date.now()
|
||||
return cachedProviderNames
|
||||
} catch (error) {
|
||||
// Silently return empty when config doesn't exist (e.g. Docker containers)
|
||||
const code = (error as NodeJS.ErrnoException)?.code
|
||||
if (code !== 'ENOENT') {
|
||||
if (import.meta.env.DEV)
|
||||
console.error('Failed to read Gateway config for provider names:', error)
|
||||
}
|
||||
if (import.meta.env.DEV)
|
||||
console.error('Failed to read Gateway config for provider names:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -118,11 +181,9 @@ export function getConfiguredProviders(): Array<string> {
|
||||
export function getConfiguredModelIds(): Set<string> {
|
||||
if (cachedModelIds && !isCacheStale()) return cachedModelIds
|
||||
|
||||
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json')
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(configPath, 'utf8')
|
||||
const config = JSON.parse(raw) as GatewayConfig
|
||||
const config = readGatewayConfig()
|
||||
if (!config) return new Set()
|
||||
|
||||
const modelIds = new Set<string>()
|
||||
|
||||
@@ -163,11 +224,8 @@ export function getConfiguredModelIds(): Set<string> {
|
||||
cacheTimestamp = Date.now()
|
||||
return cachedModelIds
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException)?.code
|
||||
if (code !== 'ENOENT') {
|
||||
if (import.meta.env.DEV)
|
||||
console.error('Failed to read Gateway config for model IDs:', error)
|
||||
}
|
||||
if (import.meta.env.DEV)
|
||||
console.error('Failed to read Gateway config for model IDs:', error)
|
||||
return new Set()
|
||||
}
|
||||
}
|
||||
@@ -187,11 +245,10 @@ type ConfigModelEntry = {
|
||||
* in the model switcher even if the gateway's auto-discovery doesn't return them.
|
||||
*/
|
||||
export function getConfiguredModelsFromConfig(): ConfigModelEntry[] {
|
||||
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json')
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(configPath, 'utf8')
|
||||
const config = JSON.parse(raw) as GatewayConfig
|
||||
const config = readGatewayConfig()
|
||||
if (!config) return []
|
||||
|
||||
const results: ConfigModelEntry[] = []
|
||||
|
||||
if (config.models?.providers) {
|
||||
|
||||
Reference in New Issue
Block a user