@2en/clawly-plugins 1.29.0 → 1.30.0-beta.0
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.
- package/clawly-config-defaults.json5 +118 -0
- package/config-setup.ts +90 -2
- package/gateway/config-repair.ts +17 -84
- package/model-gateway-setup.ts +39 -304
- package/package.json +5 -1
- package/resolve-gateway-credentials.ts +12 -13
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Static defaults for clawly-plugins.
|
|
3
|
+
// Deep-merged into openclaw.json via $include — sibling keys in the main
|
|
4
|
+
// config override these values. Arrays are concatenated, so the main config
|
|
5
|
+
// must NOT duplicate arrays defined here.
|
|
6
|
+
|
|
7
|
+
// models.providers — how to connect (endpoint, auth, capability declarations)
|
|
8
|
+
models: {
|
|
9
|
+
providers: {
|
|
10
|
+
"clawly-model-gateway": {
|
|
11
|
+
baseUrl: "${CLAWLY_MODEL_GATEWAY_BASE}/v1",
|
|
12
|
+
apiKey: "${CLAWLY_MODEL_GATEWAY_API_KEY}",
|
|
13
|
+
api: "openai-completions",
|
|
14
|
+
models: [
|
|
15
|
+
{
|
|
16
|
+
id: "moonshotai/kimi-k2.5",
|
|
17
|
+
name: "moonshotai/kimi-k2.5",
|
|
18
|
+
input: ["text", "image"],
|
|
19
|
+
contextWindow: 262144,
|
|
20
|
+
maxTokens: 65535,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "google/gemini-2.5-pro",
|
|
24
|
+
name: "google/gemini-2.5-pro",
|
|
25
|
+
input: ["text", "image"],
|
|
26
|
+
contextWindow: 1048576,
|
|
27
|
+
maxTokens: 65536,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "google/gemini-3-pro-preview",
|
|
31
|
+
name: "google/gemini-3-pro-preview",
|
|
32
|
+
input: ["text", "image"],
|
|
33
|
+
contextWindow: 1048576,
|
|
34
|
+
maxTokens: 65536,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "anthropic/claude-sonnet-4.6",
|
|
38
|
+
name: "anthropic/claude-sonnet-4.6",
|
|
39
|
+
input: ["text", "image"],
|
|
40
|
+
contextWindow: 1000000,
|
|
41
|
+
maxTokens: 128000,
|
|
42
|
+
// api: 'anthropic-messages', // TODO: uncomment once model gateway Anthropic Messages API support is out of testing
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "anthropic/claude-opus-4.6",
|
|
46
|
+
name: "anthropic/claude-opus-4.6",
|
|
47
|
+
input: ["text", "image"],
|
|
48
|
+
contextWindow: 1000000,
|
|
49
|
+
maxTokens: 128000,
|
|
50
|
+
// api: 'anthropic-messages', // TODO: uncomment once model gateway Anthropic Messages API support is out of testing
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "openai/gpt-5.4",
|
|
54
|
+
name: "openai/gpt-5.4",
|
|
55
|
+
input: ["text", "image"],
|
|
56
|
+
contextWindow: 1050000,
|
|
57
|
+
maxTokens: 128000,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "minimax/minimax-m2.5",
|
|
61
|
+
name: "minimax/minimax-m2.5",
|
|
62
|
+
input: ["text"],
|
|
63
|
+
contextWindow: 196608,
|
|
64
|
+
maxTokens: 196608,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "minimax/minimax-m2.1",
|
|
68
|
+
name: "minimax/minimax-m2.1",
|
|
69
|
+
input: ["text"],
|
|
70
|
+
contextWindow: 196608,
|
|
71
|
+
maxTokens: 196608,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "qwen/qwen3.5-plus-02-15",
|
|
75
|
+
name: "qwen/qwen3.5-plus-02-15",
|
|
76
|
+
input: ["text", "image"],
|
|
77
|
+
contextWindow: 1000000,
|
|
78
|
+
maxTokens: 65536,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "z-ai/glm-5",
|
|
82
|
+
name: "z-ai/glm-5",
|
|
83
|
+
input: ["text"],
|
|
84
|
+
contextWindow: 202752,
|
|
85
|
+
maxTokens: 131072,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// agents.defaults.models — which models the agent can use, display names,
|
|
93
|
+
// and per-model overrides. The whitelist only constrains the UI model picker
|
|
94
|
+
// and fallback lists.
|
|
95
|
+
agents: {
|
|
96
|
+
defaults: {
|
|
97
|
+
model: {
|
|
98
|
+
primary: "clawly-model-gateway/anthropic/claude-sonnet-4.6",
|
|
99
|
+
},
|
|
100
|
+
imageModel: {
|
|
101
|
+
primary: "clawly-model-gateway/qwen/qwen3.5-flash-02-23",
|
|
102
|
+
fallbacks: [],
|
|
103
|
+
},
|
|
104
|
+
models: {
|
|
105
|
+
"clawly-model-gateway/moonshotai/kimi-k2.5": { alias: "Kimi K2.5" },
|
|
106
|
+
"clawly-model-gateway/google/gemini-2.5-pro": { alias: "Gemini 2.5 Pro" },
|
|
107
|
+
"clawly-model-gateway/google/gemini-3-pro-preview": { alias: "Gemini 3 Pro Preview" },
|
|
108
|
+
"clawly-model-gateway/anthropic/claude-sonnet-4.6": { alias: "Claude Sonnet 4.6" },
|
|
109
|
+
"clawly-model-gateway/anthropic/claude-opus-4.6": { alias: "Claude Opus 4.6" },
|
|
110
|
+
"clawly-model-gateway/openai/gpt-5.4": { alias: "GPT-5.4" },
|
|
111
|
+
"clawly-model-gateway/minimax/minimax-m2.5": { alias: "MiniMax M2.5" },
|
|
112
|
+
"clawly-model-gateway/minimax/minimax-m2.1": { alias: "MiniMax M2.1" },
|
|
113
|
+
"clawly-model-gateway/qwen/qwen3.5-plus-02-15": { alias: "Qwen 3.5 Plus" },
|
|
114
|
+
"clawly-model-gateway/z-ai/glm-5": { alias: "GLM-5" },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}
|
package/config-setup.ts
CHANGED
|
@@ -22,9 +22,12 @@ import type {PluginApi} from './index'
|
|
|
22
22
|
import {autoSanitizeSession} from './gateway/session-sanitize'
|
|
23
23
|
import {resolveGatewayCredentials} from './resolve-gateway-credentials'
|
|
24
24
|
import {
|
|
25
|
+
ENV_KEY_API_KEY,
|
|
26
|
+
ENV_KEY_BASE,
|
|
25
27
|
PROVIDER_NAME,
|
|
26
28
|
patchModelGateway,
|
|
27
29
|
readOpenclawConfig,
|
|
30
|
+
stripPathname,
|
|
28
31
|
writeOpenclawConfig,
|
|
29
32
|
} from './model-gateway-setup'
|
|
30
33
|
|
|
@@ -542,6 +545,44 @@ export function patchHeartbeat(config: Record<string, unknown>, pc: ConfigPlugin
|
|
|
542
545
|
// return false
|
|
543
546
|
// }
|
|
544
547
|
|
|
548
|
+
export function patchEnvVars(
|
|
549
|
+
config: import('./types/openclaw').OpenClawConfig,
|
|
550
|
+
pc: ConfigPluginConfig,
|
|
551
|
+
api: PluginApi,
|
|
552
|
+
): boolean {
|
|
553
|
+
// TEMP fallback: recover from legacy provider entry until all sprites are migrated.
|
|
554
|
+
const provider = config.models?.providers?.[PROVIDER_NAME]
|
|
555
|
+
|
|
556
|
+
let dirty = false
|
|
557
|
+
if (!config.env) {
|
|
558
|
+
config.env = {}
|
|
559
|
+
}
|
|
560
|
+
if (!config.env.vars) {
|
|
561
|
+
config.env.vars = {}
|
|
562
|
+
}
|
|
563
|
+
const vars = config.env.vars
|
|
564
|
+
|
|
565
|
+
// CLAWLY_MODEL_GATEWAY_BASE
|
|
566
|
+
if (!vars[ENV_KEY_BASE]) {
|
|
567
|
+
const urlValue = pc.modelGatewayBaseUrl || provider?.baseUrl || ''
|
|
568
|
+
if (urlValue) {
|
|
569
|
+
vars[ENV_KEY_BASE] = stripPathname(urlValue)
|
|
570
|
+
dirty = true
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// CLAWLY_MODEL_GATEWAY_API_KEY
|
|
575
|
+
if (!vars[ENV_KEY_API_KEY]) {
|
|
576
|
+
const tokenValue = pc.modelGatewayToken || (provider?.apiKey as string) || ''
|
|
577
|
+
if (tokenValue) {
|
|
578
|
+
vars[ENV_KEY_API_KEY] = tokenValue
|
|
579
|
+
dirty = true
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return dirty
|
|
584
|
+
}
|
|
585
|
+
|
|
545
586
|
export function patchSession(config: Record<string, unknown>): boolean {
|
|
546
587
|
let dirty = false
|
|
547
588
|
|
|
@@ -585,6 +626,50 @@ export function patchSession(config: Record<string, unknown>): boolean {
|
|
|
585
626
|
return dirty
|
|
586
627
|
}
|
|
587
628
|
|
|
629
|
+
const INCLUDE_PATH = './extensions/clawly-plugins/clawly-config-defaults.json5'
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Ensures our defaults file is listed in the config's `$include` array.
|
|
633
|
+
* Returns true if `$include` was modified.
|
|
634
|
+
*/
|
|
635
|
+
export function ensureInclude(config: Record<string, unknown>): boolean {
|
|
636
|
+
const existing = config.$include
|
|
637
|
+
let includes: string[]
|
|
638
|
+
|
|
639
|
+
if (Array.isArray(existing)) {
|
|
640
|
+
includes = existing as string[]
|
|
641
|
+
} else if (typeof existing === 'string') {
|
|
642
|
+
includes = [existing]
|
|
643
|
+
} else {
|
|
644
|
+
includes = []
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (includes.includes(INCLUDE_PATH)) return false
|
|
648
|
+
|
|
649
|
+
includes.push(INCLUDE_PATH)
|
|
650
|
+
config.$include = includes.length === 1 ? includes[0] : includes
|
|
651
|
+
return true
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Restrict the $include defaults file to owner-only (0o600), matching
|
|
656
|
+
* openclaw.json. npm/plugin-install creates files with 0o644 by default,
|
|
657
|
+
* which would leave config readable by other local users.
|
|
658
|
+
*/
|
|
659
|
+
function hardenIncludePermissions(stateDir: string, api: PluginApi): void {
|
|
660
|
+
const includePath = path.join(stateDir, INCLUDE_PATH)
|
|
661
|
+
try {
|
|
662
|
+
const stat = fs.statSync(includePath)
|
|
663
|
+
const mode = stat.mode & 0o777
|
|
664
|
+
if (mode !== 0o600) {
|
|
665
|
+
fs.chmodSync(includePath, 0o600)
|
|
666
|
+
api.logger.info(`Hardened ${INCLUDE_PATH} permissions to 0600.`)
|
|
667
|
+
}
|
|
668
|
+
} catch {
|
|
669
|
+
// File may not exist yet (first install before plugin copies files)
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
588
673
|
const PLUGIN_ID = 'clawly-plugins'
|
|
589
674
|
const NPM_PKG_NAME = '@2en/clawly-plugins'
|
|
590
675
|
|
|
@@ -663,7 +748,7 @@ function reconcileRuntimeConfig(
|
|
|
663
748
|
// Ensure gateway credentials are available to all patchers.
|
|
664
749
|
// After a force-update, runtime pluginConfig may be empty and on-disk
|
|
665
750
|
// file plugin config may not have credentials — but they may exist in
|
|
666
|
-
//
|
|
751
|
+
// env.vars (backfilled by prior runs).
|
|
667
752
|
// resolveGatewayCredentials checks all three sources.
|
|
668
753
|
if (!pc.modelGatewayBaseUrl || !pc.modelGatewayToken) {
|
|
669
754
|
const creds = resolveGatewayCredentials(api, config)
|
|
@@ -673,6 +758,7 @@ function reconcileRuntimeConfig(
|
|
|
673
758
|
}
|
|
674
759
|
|
|
675
760
|
let dirty = false
|
|
761
|
+
dirty = patchEnvVars(config as import('./types/openclaw').OpenClawConfig, pc, api) || dirty
|
|
676
762
|
dirty = patchAgent(config, pc) || dirty
|
|
677
763
|
dirty = patchContextPruning(config) || dirty
|
|
678
764
|
dirty = patchHeartbeat(config, pc) || dirty
|
|
@@ -682,7 +768,7 @@ function reconcileRuntimeConfig(
|
|
|
682
768
|
dirty = patchTts(config, pc) || dirty
|
|
683
769
|
dirty = patchWebSearch(config, pc) || dirty
|
|
684
770
|
dirty = patchMemorySearch(config, pc) || dirty
|
|
685
|
-
dirty = patchModelGateway(config, api
|
|
771
|
+
dirty = patchModelGateway(config as import('./types/openclaw').OpenClawConfig, api) || dirty
|
|
686
772
|
return dirty
|
|
687
773
|
}
|
|
688
774
|
|
|
@@ -702,6 +788,8 @@ export function setupConfig(api: PluginApi): void {
|
|
|
702
788
|
const pc = toPCMerged(api, config)
|
|
703
789
|
|
|
704
790
|
let dirty = false
|
|
791
|
+
dirty = ensureInclude(config) || dirty
|
|
792
|
+
hardenIncludePermissions(stateDir, api)
|
|
705
793
|
dirty = repairLegacyProvisionState(api, config, stateDir) || dirty
|
|
706
794
|
dirty = reconcileRuntimeConfig(api, config, pc) || dirty
|
|
707
795
|
|
package/gateway/config-repair.ts
CHANGED
|
@@ -10,8 +10,8 @@ import path from 'node:path'
|
|
|
10
10
|
|
|
11
11
|
import type {PluginApi} from '../types'
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
ENV_KEY_API_KEY,
|
|
14
|
+
ENV_KEY_BASE,
|
|
15
15
|
readOpenclawConfig,
|
|
16
16
|
writeOpenclawConfig,
|
|
17
17
|
} from '../model-gateway-setup'
|
|
@@ -33,7 +33,7 @@ export function registerConfigRepair(api: PluginApi) {
|
|
|
33
33
|
const configPath = path.join(stateDir, 'openclaw.json')
|
|
34
34
|
const config = readOpenclawConfig(configPath)
|
|
35
35
|
|
|
36
|
-
const gw = resolveGatewayCredentials(api, config)
|
|
36
|
+
const gw = resolveGatewayCredentials(api, config as Record<string, unknown>)
|
|
37
37
|
if (!gw) {
|
|
38
38
|
respond(true, {
|
|
39
39
|
...(dryRun ? {ok: false} : {repaired: false}),
|
|
@@ -42,102 +42,35 @@ export function registerConfigRepair(api: PluginApi) {
|
|
|
42
42
|
return
|
|
43
43
|
}
|
|
44
44
|
const {baseUrl, token} = gw
|
|
45
|
-
const
|
|
46
|
-
const provider = providers?.[PROVIDER_NAME] as Record<string, unknown> | undefined
|
|
45
|
+
const vars = config.env?.vars
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
const currentBaseUrl = typeof provider?.baseUrl === 'string' ? provider.baseUrl : ''
|
|
50
|
-
const currentApiKey = typeof provider?.apiKey === 'string' ? provider.apiKey : ''
|
|
51
|
-
|
|
52
|
-
const needsRepair = !provider || !currentBaseUrl || !currentApiKey
|
|
53
|
-
|
|
54
|
-
if (!needsRepair) {
|
|
47
|
+
if (vars?.[ENV_KEY_BASE] && vars?.[ENV_KEY_API_KEY]) {
|
|
55
48
|
respond(true, {
|
|
56
49
|
...(dryRun ? {ok: true} : {repaired: false}),
|
|
57
|
-
detail: '
|
|
50
|
+
detail: 'env.vars credentials intact',
|
|
58
51
|
})
|
|
59
52
|
return
|
|
60
53
|
}
|
|
61
54
|
|
|
62
|
-
// Dry-run: report status without mutating
|
|
63
55
|
if (dryRun) {
|
|
64
|
-
|
|
65
|
-
if (!provider) missing.push('provider entry')
|
|
66
|
-
else {
|
|
67
|
-
if (!currentBaseUrl) missing.push('baseUrl')
|
|
68
|
-
if (!currentApiKey) missing.push('apiKey')
|
|
69
|
-
}
|
|
70
|
-
respond(true, {ok: false, detail: `Missing: ${missing.join(', ')}`})
|
|
56
|
+
respond(true, {ok: false, detail: 'Missing: env.vars credentials'})
|
|
71
57
|
return
|
|
72
58
|
}
|
|
73
59
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
? defaultModelFull.slice(prefix.length)
|
|
82
|
-
: defaultModelFull
|
|
83
|
-
|
|
84
|
-
const defaultIds = new Set([defaultModel])
|
|
85
|
-
const extraModels = EXTRA_GATEWAY_MODELS.filter((m) => !defaultIds.has(m.id)).map(
|
|
86
|
-
({id, name, input, contextWindow, maxTokens, api}) => ({
|
|
87
|
-
id,
|
|
88
|
-
name,
|
|
89
|
-
input,
|
|
90
|
-
contextWindow,
|
|
91
|
-
maxTokens,
|
|
92
|
-
...(api ? {api} : {}),
|
|
93
|
-
}),
|
|
94
|
-
)
|
|
95
|
-
const extraMatch = EXTRA_GATEWAY_MODELS.find((m) => m.id === defaultModel)
|
|
96
|
-
const models = !defaultModel
|
|
97
|
-
? (provider?.models ?? [])
|
|
98
|
-
: [
|
|
99
|
-
{
|
|
100
|
-
id: defaultModel,
|
|
101
|
-
name: defaultModel,
|
|
102
|
-
input: extraMatch ? [...extraMatch.input] : (['text', 'image'] as string[]),
|
|
103
|
-
...(extraMatch && {
|
|
104
|
-
contextWindow: extraMatch.contextWindow,
|
|
105
|
-
maxTokens: extraMatch.maxTokens,
|
|
106
|
-
}),
|
|
107
|
-
...(extraMatch?.api ? {api: extraMatch.api} : {}),
|
|
108
|
-
},
|
|
109
|
-
...extraModels,
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
if (!config.models) config.models = {}
|
|
113
|
-
if (!(config.models as any).providers) (config.models as any).providers = {}
|
|
114
|
-
;(config.models as any).providers[PROVIDER_NAME] = {
|
|
115
|
-
...(provider ?? {}),
|
|
116
|
-
baseUrl,
|
|
117
|
-
apiKey: token,
|
|
118
|
-
api: 'openai-completions',
|
|
119
|
-
models,
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Write agents.defaults.models alias map (mirrors model-gateway-setup)
|
|
123
|
-
if (defaultModel) {
|
|
124
|
-
const agents = (config.agents ?? {}) as any
|
|
125
|
-
const defaults = agents.defaults ?? {}
|
|
126
|
-
const modelsMap: Record<string, {alias: string}> = {
|
|
127
|
-
[`${PROVIDER_NAME}/${defaultModel}`]: {alias: defaultModel},
|
|
128
|
-
}
|
|
129
|
-
for (const m of EXTRA_GATEWAY_MODELS) {
|
|
130
|
-
modelsMap[`${PROVIDER_NAME}/${m.id}`] = {alias: m.alias}
|
|
131
|
-
}
|
|
132
|
-
defaults.models = modelsMap
|
|
133
|
-
agents.defaults = defaults
|
|
134
|
-
config.agents = agents
|
|
60
|
+
// Write credentials into env.vars for $include resolution.
|
|
61
|
+
if (!config.env) config.env = {}
|
|
62
|
+
if (!config.env.vars) config.env.vars = {}
|
|
63
|
+
try {
|
|
64
|
+
config.env.vars[ENV_KEY_BASE] = new URL(baseUrl).origin
|
|
65
|
+
} catch {
|
|
66
|
+
config.env.vars[ENV_KEY_BASE] = baseUrl
|
|
135
67
|
}
|
|
68
|
+
config.env.vars[ENV_KEY_API_KEY] = token
|
|
136
69
|
|
|
137
70
|
try {
|
|
138
71
|
writeOpenclawConfig(configPath, config)
|
|
139
|
-
api.logger.info(`config.repair: restored
|
|
140
|
-
respond(true, {repaired: true, detail: '
|
|
72
|
+
api.logger.info(`config.repair: restored env.vars credentials`)
|
|
73
|
+
respond(true, {repaired: true, detail: 'env.vars credentials restored'})
|
|
141
74
|
} catch (err) {
|
|
142
75
|
const msg = err instanceof Error ? err.message : String(err)
|
|
143
76
|
api.logger.error(`config.repair: write failed — ${msg}`)
|
package/model-gateway-setup.ts
CHANGED
|
@@ -1,122 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reconciles the `clawly-model-gateway` provider in openclaw.json
|
|
3
|
-
* pluginConfig inputs and the current runtime-facing agent defaults.
|
|
2
|
+
* Reconciles the `clawly-model-gateway` provider in openclaw.json.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Static provider config (baseUrl, apiKey, api, models, aliases) is declarative
|
|
5
|
+
* via $include (clawly-config-defaults.json5) with env var references. This
|
|
6
|
+
* file only handles:
|
|
7
|
+
* - Migration: delete inline models, aliases from provider (now from $include)
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* public model view. Provider routing and upstream model mapping remain
|
|
11
|
-
* repository-internal concerns owned by the backend.
|
|
9
|
+
* env.vars backfill is handled by patchEnvVars in config-setup.ts.
|
|
12
10
|
*/
|
|
13
11
|
|
|
14
12
|
import fs from 'node:fs'
|
|
15
|
-
import path from 'node:path'
|
|
16
13
|
|
|
17
14
|
import type {PluginApi} from './index'
|
|
15
|
+
import type {OpenClawConfig} from './types/openclaw'
|
|
18
16
|
|
|
19
17
|
export const PROVIDER_NAME = 'clawly-model-gateway'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const PUBLIC_GATEWAY_MODELS = [
|
|
26
|
-
{
|
|
27
|
-
canonicalId: 'moonshotai/kimi-k2.5',
|
|
28
|
-
alias: 'Kimi K2.5',
|
|
29
|
-
input: ['text', 'image'],
|
|
30
|
-
contextWindow: 262_144,
|
|
31
|
-
maxTokens: 65_535,
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
canonicalId: 'google/gemini-2.5-pro',
|
|
35
|
-
alias: 'Gemini 2.5 Pro',
|
|
36
|
-
input: ['text', 'image'],
|
|
37
|
-
contextWindow: 1_048_576,
|
|
38
|
-
maxTokens: 65_536,
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
canonicalId: 'google/gemini-3-pro-preview',
|
|
42
|
-
alias: 'Gemini 3 Pro Preview',
|
|
43
|
-
input: ['text', 'image'],
|
|
44
|
-
contextWindow: 1_048_576,
|
|
45
|
-
maxTokens: 65_536,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
canonicalId: 'anthropic/claude-sonnet-4.6',
|
|
49
|
-
alias: 'Claude Sonnet 4.6',
|
|
50
|
-
input: ['text', 'image'],
|
|
51
|
-
contextWindow: 1_000_000,
|
|
52
|
-
maxTokens: 128_000,
|
|
53
|
-
// api: 'anthropic-messages', // TODO: uncomment once model gateway Anthropic Messages API support is out of testing
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
canonicalId: 'anthropic/claude-opus-4.6',
|
|
57
|
-
alias: 'Claude Opus 4.6',
|
|
58
|
-
input: ['text', 'image'],
|
|
59
|
-
contextWindow: 1_000_000,
|
|
60
|
-
maxTokens: 128_000,
|
|
61
|
-
// api: 'anthropic-messages',
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
canonicalId: 'openai/gpt-5.4',
|
|
65
|
-
alias: 'GPT-5.4',
|
|
66
|
-
input: ['text', 'image'],
|
|
67
|
-
contextWindow: 1_050_000,
|
|
68
|
-
maxTokens: 128_000,
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
canonicalId: 'minimax/minimax-m2.5',
|
|
72
|
-
alias: 'MiniMax M2.5',
|
|
73
|
-
input: ['text'],
|
|
74
|
-
contextWindow: 196_608,
|
|
75
|
-
maxTokens: 196_608,
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
canonicalId: 'minimax/minimax-m2.1',
|
|
79
|
-
alias: 'MiniMax M2.1',
|
|
80
|
-
input: ['text'],
|
|
81
|
-
contextWindow: 196_608,
|
|
82
|
-
maxTokens: 196_608,
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
canonicalId: 'qwen/qwen3.5-plus-02-15',
|
|
86
|
-
alias: 'Qwen 3.5 Plus',
|
|
87
|
-
input: ['text', 'image'],
|
|
88
|
-
contextWindow: 1_000_000,
|
|
89
|
-
maxTokens: 65_536,
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
canonicalId: 'z-ai/glm-5',
|
|
93
|
-
alias: 'GLM-5',
|
|
94
|
-
input: ['text'],
|
|
95
|
-
contextWindow: 202_752,
|
|
96
|
-
maxTokens: 131_072,
|
|
97
|
-
},
|
|
98
|
-
] as const
|
|
99
|
-
|
|
100
|
-
/** Additional models available through the model gateway (beyond env-configured defaults). */
|
|
101
|
-
export const EXTRA_GATEWAY_MODELS: Array<{
|
|
102
|
-
id: string
|
|
103
|
-
name: string
|
|
104
|
-
alias: string
|
|
105
|
-
input: string[]
|
|
106
|
-
contextWindow: number
|
|
107
|
-
maxTokens: number
|
|
108
|
-
api?: string
|
|
109
|
-
}> = PUBLIC_GATEWAY_MODELS.map((model) => ({
|
|
110
|
-
id: model.canonicalId,
|
|
111
|
-
name: model.canonicalId,
|
|
112
|
-
alias: model.alias,
|
|
113
|
-
input: [...model.input],
|
|
114
|
-
contextWindow: model.contextWindow,
|
|
115
|
-
maxTokens: model.maxTokens,
|
|
116
|
-
...('api' in model && model.api ? {api: model.api} : {}),
|
|
117
|
-
}))
|
|
118
|
-
|
|
119
|
-
export function readOpenclawConfig(configPath: string): Record<string, unknown> {
|
|
18
|
+
export const ENV_KEY_BASE = 'CLAWLY_MODEL_GATEWAY_BASE'
|
|
19
|
+
export const ENV_KEY_API_KEY = 'CLAWLY_MODEL_GATEWAY_API_KEY'
|
|
20
|
+
export function readOpenclawConfig(configPath: string): OpenClawConfig {
|
|
120
21
|
try {
|
|
121
22
|
return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
122
23
|
} catch {
|
|
@@ -124,211 +25,45 @@ export function readOpenclawConfig(configPath: string): Record<string, unknown>
|
|
|
124
25
|
}
|
|
125
26
|
}
|
|
126
27
|
|
|
127
|
-
export function writeOpenclawConfig(configPath: string, config:
|
|
28
|
+
export function writeOpenclawConfig(configPath: string, config: OpenClawConfig) {
|
|
128
29
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
|
|
129
30
|
}
|
|
130
31
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} {
|
|
139
|
-
const extra = EXTRA_GATEWAY_MODELS.find((model) => model.id === id)
|
|
140
|
-
return {
|
|
141
|
-
id,
|
|
142
|
-
name: id,
|
|
143
|
-
input: extra ? [...extra.input] : ['text', 'image'],
|
|
144
|
-
...(extra ? {contextWindow: extra.contextWindow, maxTokens: extra.maxTokens} : {}),
|
|
145
|
-
...(extra?.api ? {api: extra.api} : {}),
|
|
32
|
+
/** Strip pathname from a URL, returning just the origin (scheme + host + port). */
|
|
33
|
+
export function stripPathname(url: string): string {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(url)
|
|
36
|
+
return parsed.origin
|
|
37
|
+
} catch {
|
|
38
|
+
return url
|
|
146
39
|
}
|
|
147
40
|
}
|
|
148
41
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
): boolean {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// credentials in openclaw.json directly, not in pluginConfig.
|
|
163
|
-
const existingProvider = (config.models as any)?.providers?.[PROVIDER_NAME]
|
|
164
|
-
if (existingProvider) {
|
|
165
|
-
let dirty = false
|
|
166
|
-
const existingModels: Array<{id: string}> = existingProvider.models ?? []
|
|
167
|
-
const existingIds = new Set(existingModels.map((m: {id: string}) => m.id))
|
|
168
|
-
|
|
169
|
-
// Append any missing extra models
|
|
170
|
-
for (const m of EXTRA_GATEWAY_MODELS) {
|
|
171
|
-
if (!existingIds.has(m.id)) {
|
|
172
|
-
existingModels.push({
|
|
173
|
-
id: m.id,
|
|
174
|
-
name: m.name,
|
|
175
|
-
input: m.input,
|
|
176
|
-
contextWindow: m.contextWindow,
|
|
177
|
-
maxTokens: m.maxTokens,
|
|
178
|
-
...(m.api ? {api: m.api} : {}),
|
|
179
|
-
} as any)
|
|
180
|
-
dirty = true
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Backfill contextWindow/maxTokens and api on existing models that lack them
|
|
185
|
-
const extraLookup = new Map(EXTRA_GATEWAY_MODELS.map((m) => [m.id, m]))
|
|
186
|
-
for (const m of existingModels) {
|
|
187
|
-
const extra = extraLookup.get(m.id)
|
|
188
|
-
if (!extra) continue
|
|
189
|
-
if (
|
|
190
|
-
(m as any).contextWindow !== extra.contextWindow ||
|
|
191
|
-
(m as any).maxTokens !== extra.maxTokens
|
|
192
|
-
) {
|
|
193
|
-
;(m as any).contextWindow = extra.contextWindow
|
|
194
|
-
;(m as any).maxTokens = extra.maxTokens
|
|
195
|
-
dirty = true
|
|
196
|
-
}
|
|
197
|
-
if (extra.api && (m as any).api !== extra.api) {
|
|
198
|
-
;(m as any).api = extra.api
|
|
199
|
-
dirty = true
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Ensure aliases exist for ALL models (existing + extras)
|
|
204
|
-
const agents = (config.agents ?? {}) as any
|
|
205
|
-
const defaults = agents.defaults ?? {}
|
|
206
|
-
const existingAliases: Record<string, {alias: string}> = defaults.models ?? {}
|
|
207
|
-
for (const m of existingModels) {
|
|
208
|
-
const key = `${PROVIDER_NAME}/${m.id}`
|
|
209
|
-
if (!existingAliases[key]) {
|
|
210
|
-
const extra = EXTRA_GATEWAY_MODELS.find((e) => e.id === m.id)
|
|
211
|
-
existingAliases[key] = {alias: extra?.alias ?? m.id}
|
|
212
|
-
dirty = true
|
|
213
|
-
}
|
|
214
|
-
}
|
|
42
|
+
/**
|
|
43
|
+
* @deprecated Migration-only — remove once all sprites have been reprovisioned
|
|
44
|
+
* and no inline models/aliases remain in openclaw.json.
|
|
45
|
+
*/
|
|
46
|
+
export function patchModelGateway(config: OpenClawConfig, _api: PluginApi): boolean {
|
|
47
|
+
let dirty = false
|
|
48
|
+
const provider = config.models?.providers?.[PROVIDER_NAME]
|
|
49
|
+
|
|
50
|
+
// Migration: delete inline models array (now from $include).
|
|
51
|
+
if (provider?.models) {
|
|
52
|
+
delete (provider as Record<string, unknown>).models
|
|
53
|
+
dirty = true
|
|
54
|
+
}
|
|
215
55
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
(
|
|
222
|
-
|
|
223
|
-
const entries = (config.plugins as any)?.entries?.['clawly-plugins']
|
|
224
|
-
if (entries) {
|
|
225
|
-
entries.config = {
|
|
226
|
-
...entries.config,
|
|
227
|
-
modelGatewayBaseUrl: existingProvider.baseUrl,
|
|
228
|
-
modelGatewayToken: existingProvider.apiKey,
|
|
229
|
-
}
|
|
56
|
+
// Migration: delete aliases starting with our provider prefix (now from $include).
|
|
57
|
+
const aliasMap = config.agents?.defaults?.models
|
|
58
|
+
if (aliasMap) {
|
|
59
|
+
const prefix = `${PROVIDER_NAME}/`
|
|
60
|
+
for (const key of Object.keys(aliasMap)) {
|
|
61
|
+
if (key.startsWith(prefix)) {
|
|
62
|
+
delete aliasMap[key]
|
|
230
63
|
dirty = true
|
|
231
|
-
api.logger.info(
|
|
232
|
-
'Model gateway: backfilled pluginConfig from existing provider credentials.',
|
|
233
|
-
)
|
|
234
64
|
}
|
|
235
65
|
}
|
|
236
|
-
|
|
237
|
-
if (dirty) {
|
|
238
|
-
existingProvider.models = existingModels
|
|
239
|
-
defaults.models = existingAliases
|
|
240
|
-
agents.defaults = defaults
|
|
241
|
-
config.agents = agents
|
|
242
|
-
api.logger.info('Model gateway updated.')
|
|
243
|
-
} else {
|
|
244
|
-
api.logger.info('Model gateway provider already configured.')
|
|
245
|
-
}
|
|
246
|
-
return dirty
|
|
247
66
|
}
|
|
248
67
|
|
|
249
|
-
|
|
250
|
-
const baseUrl =
|
|
251
|
-
typeof cfg?.modelGatewayBaseUrl === 'string' ? cfg.modelGatewayBaseUrl.replace(/\/$/, '') : ''
|
|
252
|
-
const token = typeof cfg?.modelGatewayToken === 'string' ? cfg.modelGatewayToken : ''
|
|
253
|
-
|
|
254
|
-
if (!baseUrl || !token) {
|
|
255
|
-
api.logger.info('Model gateway not configured (missing baseUrl or token), skipping.')
|
|
256
|
-
return false
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Derive default model ID from agents.defaults.
|
|
260
|
-
// Image fallback is handled server-side by the model-gateway proxy, so only
|
|
261
|
-
// the default chat model needs to be registered in the provider's model list.
|
|
262
|
-
const defaultModelFull: string = (config.agents as any)?.defaults?.model?.primary ?? ''
|
|
263
|
-
|
|
264
|
-
const prefix = `${PROVIDER_NAME}/`
|
|
265
|
-
const defaultModel = defaultModelFull.startsWith(prefix)
|
|
266
|
-
? defaultModelFull.slice(prefix.length)
|
|
267
|
-
: defaultModelFull
|
|
268
|
-
|
|
269
|
-
if (!defaultModel) {
|
|
270
|
-
api.logger.warn('No default model found in agents.defaults — model gateway setup skipped.')
|
|
271
|
-
return false
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const defaultModels = [buildProviderModel(defaultModel)]
|
|
275
|
-
|
|
276
|
-
const defaultIds = new Set(defaultModels.map((m) => m.id))
|
|
277
|
-
const models = [
|
|
278
|
-
...defaultModels,
|
|
279
|
-
...EXTRA_GATEWAY_MODELS.filter((m) => !defaultIds.has(m.id)).map(({id}) =>
|
|
280
|
-
buildProviderModel(id),
|
|
281
|
-
),
|
|
282
|
-
]
|
|
283
|
-
|
|
284
|
-
if (!config.models) config.models = {}
|
|
285
|
-
if (!(config.models as any).providers) (config.models as any).providers = {}
|
|
286
|
-
;(config.models as any).providers[PROVIDER_NAME] = {
|
|
287
|
-
baseUrl,
|
|
288
|
-
apiKey: token,
|
|
289
|
-
api: 'openai-completions',
|
|
290
|
-
models,
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Write agents.defaults.models map for UI dropdown aliases
|
|
294
|
-
const agents = (config.agents ?? {}) as any
|
|
295
|
-
const defaults = agents.defaults ?? {}
|
|
296
|
-
const modelsMap: Record<string, {alias: string}> = {
|
|
297
|
-
[`${PROVIDER_NAME}/${defaultModel}`]: {alias: defaultModel},
|
|
298
|
-
}
|
|
299
|
-
for (const m of EXTRA_GATEWAY_MODELS) {
|
|
300
|
-
modelsMap[`${PROVIDER_NAME}/${m.id}`] = {alias: m.alias}
|
|
301
|
-
}
|
|
302
|
-
defaults.models = modelsMap
|
|
303
|
-
agents.defaults = defaults
|
|
304
|
-
config.agents = agents
|
|
305
|
-
|
|
306
|
-
api.logger.info(`Model gateway provider configured: ${baseUrl} with ${models.length} model(s).`)
|
|
307
|
-
return true
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Standalone wrapper — reads config, patches, writes. Used by tests.
|
|
312
|
-
*
|
|
313
|
-
* This helper intentionally does not apply the file-backed pluginConfig fallback
|
|
314
|
-
* from setupConfig(); production startup should go through the full runtime
|
|
315
|
-
* reconcile path there.
|
|
316
|
-
*/
|
|
317
|
-
export function setupModelGateway(api: PluginApi): void {
|
|
318
|
-
const stateDir = api.runtime.state.resolveStateDir()
|
|
319
|
-
if (!stateDir) {
|
|
320
|
-
api.logger.warn('Cannot resolve state dir — model gateway setup skipped.')
|
|
321
|
-
return
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const configPath = path.join(stateDir, 'openclaw.json')
|
|
325
|
-
const config = readOpenclawConfig(configPath)
|
|
326
|
-
|
|
327
|
-
if (!patchModelGateway(config, api)) return
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
writeOpenclawConfig(configPath, config)
|
|
331
|
-
} catch (err) {
|
|
332
|
-
api.logger.error(`Failed to setup model gateway: ${(err as Error).message}`)
|
|
333
|
-
}
|
|
68
|
+
return dirty
|
|
334
69
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2en/clawly-plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0-beta.0",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"resolve-gateway-credentials.ts",
|
|
37
37
|
"skill-command-restore.ts",
|
|
38
38
|
"openclaw.plugin.json",
|
|
39
|
+
"clawly-config-defaults.json5",
|
|
39
40
|
"internal",
|
|
40
41
|
"skills"
|
|
41
42
|
],
|
|
@@ -46,5 +47,8 @@
|
|
|
46
47
|
"extensions": [
|
|
47
48
|
"./index.ts"
|
|
48
49
|
]
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"json5": "^2.2.3"
|
|
49
53
|
}
|
|
50
54
|
}
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Fallback order:
|
|
5
5
|
* 1. Runtime pluginConfig (api.pluginConfig) — always present on fresh provision
|
|
6
|
-
* 2. On-disk
|
|
6
|
+
* 2. On-disk env.vars (CLAWLY_MODEL_GATEWAY_BASE/API_KEY) — written by setupConfig
|
|
7
7
|
* 3. On-disk file plugin config (plugins.entries.clawly-plugins.config) — written during provision
|
|
8
8
|
*
|
|
9
9
|
* After a force-update, runtime pluginConfig may be empty (it's immutable at
|
|
10
|
-
* runtime), but setupConfig backfills credentials into
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* runtime), but setupConfig backfills credentials into env.vars and file
|
|
11
|
+
* plugin config entries. This function reads from all three sources so
|
|
12
|
+
* callers always get credentials when available.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {ENV_KEY_API_KEY, ENV_KEY_BASE, readOpenclawConfig} from './model-gateway-setup'
|
|
18
18
|
import type {PluginApi} from './types'
|
|
19
19
|
|
|
20
20
|
export interface GatewayCredentials {
|
|
@@ -43,14 +43,13 @@ export function resolveGatewayCredentials(
|
|
|
43
43
|
config = readOpenclawConfig(path.join(stateDir, 'openclaw.json'))
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// 2. On-disk
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return {baseUrl: provBaseUrl.replace(/\/$/, ''), token: provApiKey}
|
|
46
|
+
// 2. On-disk env.vars (credentials stored as env vars for $include resolution)
|
|
47
|
+
const envVars = (config.env as any)?.vars as Record<string, string> | undefined
|
|
48
|
+
const envBase = typeof envVars?.[ENV_KEY_BASE] === 'string' ? envVars[ENV_KEY_BASE] : ''
|
|
49
|
+
const envBaseUrl = envBase ? `${envBase}/v1` : ''
|
|
50
|
+
const envApiKey = typeof envVars?.[ENV_KEY_API_KEY] === 'string' ? envVars[ENV_KEY_API_KEY] : ''
|
|
51
|
+
if (envBaseUrl && envApiKey) {
|
|
52
|
+
return {baseUrl: envBaseUrl.replace(/\/$/, ''), token: envApiKey}
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
// 3. On-disk file plugin config
|