@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.
@@ -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
- // models.providers.clawly-model-gateway (backfilled by prior runs).
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, pc) || dirty
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
 
@@ -10,8 +10,8 @@ import path from 'node:path'
10
10
 
11
11
  import type {PluginApi} from '../types'
12
12
  import {
13
- EXTRA_GATEWAY_MODELS,
14
- PROVIDER_NAME,
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 providers = (config.models as any)?.providers as Record<string, any> | undefined
46
- const provider = providers?.[PROVIDER_NAME] as Record<string, unknown> | undefined
45
+ const vars = config.env?.vars
47
46
 
48
- // Check current provider state
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: 'Provider credentials intact',
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
- const missing: string[] = []
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
- // Repair: derive models from agents.defaults (same logic as model-gateway-setup).
75
- // Image fallback is handled server-side by the model-gateway proxy, so only
76
- // the default chat model is registered (with image support).
77
- const defaultModelFull: string = (config.agents as any)?.defaults?.model?.primary ?? ''
78
-
79
- const prefix = `${PROVIDER_NAME}/`
80
- const defaultModel = defaultModelFull.startsWith(prefix)
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 provider credentials (baseUrl + apiKey)`)
140
- respond(true, {repaired: true, detail: 'Provider credentials restored'})
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}`)
@@ -1,122 +1,23 @@
1
1
  /**
2
- * Reconciles the `clawly-model-gateway` provider in openclaw.json from
3
- * pluginConfig inputs and the current runtime-facing agent defaults.
2
+ * Reconciles the `clawly-model-gateway` provider in openclaw.json.
4
3
  *
5
- * This is a runtime reconcile path, not a provision/bootstrap contract.
6
- * The first-boot correctness of gateway startup should not rely on this file
7
- * write winning a race against OpenClaw's internal startup snapshot timing.
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
- * Important boundary: this file intentionally keeps only the instance-visible
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
- type ModelGatewayConfigInputs = {
21
- modelGatewayBaseUrl?: string
22
- modelGatewayToken?: string
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: Record<string, unknown>) {
28
+ export function writeOpenclawConfig(configPath: string, config: OpenClawConfig) {
128
29
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
129
30
  }
130
31
 
131
- function buildProviderModel(id: string): {
132
- id: string
133
- name: string
134
- input: string[]
135
- contextWindow?: number
136
- maxTokens?: number
137
- api?: string
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
- export function patchModelGateway(
150
- config: Record<string, unknown>,
151
- api: PluginApi,
152
- inputs?: ModelGatewayConfigInputs,
153
- ): boolean {
154
- const cfg =
155
- inputs ??
156
- (api.pluginConfig as Record<string, unknown> | undefined as
157
- | ModelGatewayConfigInputs
158
- | undefined)
159
-
160
- // If provider already exists, check if extra models or aliases need updating.
161
- // This runs before the credentials check because provisioned sprites have
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
- // Backfill pluginConfig from existing provider credentials (legacy sprites
217
- // provisioned before plugin config was written during configure phase).
218
- if (
219
- existingProvider.baseUrl &&
220
- existingProvider.apiKey &&
221
- (!cfg?.modelGatewayBaseUrl || !cfg?.modelGatewayToken)
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
- // No existing provider — need pluginConfig credentials to create one
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.29.0",
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 provider entry (models.providers.clawly-model-gateway) — backfilled by setupConfig
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 the on-disk provider
11
- * and file plugin config entries. This function reads from all three sources
12
- * so callers always get credentials when available.
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 {PROVIDER_NAME, readOpenclawConfig} from './model-gateway-setup'
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 provider entry
47
- const provider = (config.models as any)?.providers?.[PROVIDER_NAME] as
48
- | Record<string, unknown>
49
- | undefined
50
- const provBaseUrl = typeof provider?.baseUrl === 'string' ? provider.baseUrl : ''
51
- const provApiKey = typeof provider?.apiKey === 'string' ? provider.apiKey : ''
52
- if (provBaseUrl && provApiKey) {
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