fix: lazy OpenAI client + canonical workspace detection from openclaw.json

Move OpenAI client initialization from module top-level to a lazy
singleton (lib/openai.ts) called inside route handlers. Prevents build
errors when gateway env vars aren't set. Also adds openclaw.json
agents.defaults.workspace as the first workspace detection source,
before falling back to filesystem paths.

Inspired by PR #21 (cherry-picked the good parts, skipped the fragile
cli-utils rewrite and the agents-registry simplification that dropped
sub-agents). Bumps to v0.8.8.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
JohnRiceML
2026-03-23 10:36:17 -05:00
co-authored by Claude Opus 4.6
parent 602e9cd56d
commit a51ac3cbe0
9 changed files with 64 additions and 38 deletions
+3 -8
View File
@@ -3,14 +3,8 @@ export const runtime = 'nodejs'
import { getAgent } from '@/lib/agents'
import { validateChatMessages } from '@/lib/validation'
import { hasImageContent, extractImageAttachments, buildTextPrompt, sendViaOpenClaw } from '@/lib/anthropic'
import OpenAI from 'openai'
import { gatewayBaseUrl } from '@/lib/env'
// Route through the OpenClaw gateway — no separate API key needed
const openai = new OpenAI({
baseURL: gatewayBaseUrl(),
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
})
import { getOpenAIClient } from '@/lib/openai'
import type OpenAI from 'openai'
const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || ''
@@ -18,6 +12,7 @@ export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const openai = getOpenAIClient()
const { id } = await params
const agent = await getAgent(id)
+3 -7
View File
@@ -1,13 +1,8 @@
export const runtime = 'nodejs'
import { getAgent } from '@/lib/agents'
import OpenAI from 'openai'
import { gatewayBaseUrl } from '@/lib/env'
const openai = new OpenAI({
baseURL: gatewayBaseUrl(),
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
})
import { getOpenAIClient } from '@/lib/openai'
import type OpenAI from 'openai'
const MAX_TITLE = 500
const MAX_DESC = 5000
@@ -27,6 +22,7 @@ export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const openai = getOpenAIClient()
const { id } = await params
const agent = await getAgent(id)
+2 -7
View File
@@ -1,14 +1,9 @@
export const runtime = 'nodejs'
import OpenAI from 'openai'
import { gatewayBaseUrl } from '@/lib/env'
const openai = new OpenAI({
baseURL: gatewayBaseUrl(),
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
})
import { getOpenAIClient } from '@/lib/openai'
export async function POST(request: Request) {
const openai = getOpenAIClient()
let formData: FormData
try {
formData = await request.formData()
+2 -7
View File
@@ -1,14 +1,9 @@
export const runtime = 'nodejs'
import OpenAI from 'openai'
import { gatewayBaseUrl } from '@/lib/env'
const openai = new OpenAI({
baseURL: gatewayBaseUrl(),
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
})
import { getOpenAIClient } from '@/lib/openai'
export async function POST(request: Request) {
const openai = getOpenAIClient()
try {
const { text, voice } = await request.json()
+20
View File
@@ -0,0 +1,20 @@
import OpenAI from 'openai'
import { gatewayBaseUrl } from './env'
/**
* Lazy-initialized OpenAI client routed through the OpenClaw gateway.
*
* Call inside route handlers, not at module top level, so builds don't
* fail when environment variables aren't set.
*/
let _openai: OpenAI | null = null
export function getOpenAIClient(): OpenAI {
if (!_openai) {
_openai = new OpenAI({
baseURL: gatewayBaseUrl(),
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN || '',
})
}
return _openai
}
+18 -5
View File
@@ -21,15 +21,28 @@ import { execSync } from 'child_process'
* Detect the default workspace path.
*
* Checks in order:
* 1. ~/.openclaw/agents/main/workspace (current agent-scoped layout)
* 2. ~/.openclaw/workspace-main (multi-agent layout, main agent)
* 3. ~/.openclaw/workspace-* (multi-agent layout, any agent)
* 4. ~/.openclaw/workspace (legacy single-workspace layout)
* 1. ~/.openclaw/openclaw.json agents.defaults.workspace (canonical)
* 2. ~/.openclaw/agents/main/workspace (current agent-scoped layout)
* 3. ~/.openclaw/workspace-main (multi-agent layout, main agent)
* 4. ~/.openclaw/workspace-* (multi-agent layout, any agent)
* 5. ~/.openclaw/workspace (legacy single-workspace layout)
*/
export function detectWorkspacePath(): string | null {
const base = join(homedir(), '.openclaw')
// 1. Current agent-scoped layout
// 1. Read from openclaw.json (canonical source)
const configPath = join(base, 'openclaw.json')
if (existsSync(configPath)) {
try {
const config = JSON.parse(readFileSync(configPath, 'utf-8'))
const ws = config?.agents?.defaults?.workspace
if (typeof ws === 'string' && existsSync(ws)) return ws
} catch {
// Invalid JSON, fall through
}
}
// 2. Current agent-scoped layout
const agentPath = join(base, 'agents', 'main', 'workspace')
if (existsSync(agentPath)) return agentPath
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "clawport-ui",
"version": "0.8.7",
"version": "0.8.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawport-ui",
"version": "0.8.7",
"version": "0.8.8",
"license": "MIT",
"dependencies": {
"@dagrejs/dagre": "^2.0.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawport-ui",
"version": "0.8.7",
"version": "0.8.8",
"description": "Open-source dashboard for managing, monitoring, and chatting with your OpenClaw AI agents.",
"homepage": "https://clawport.dev",
"repository": {
+13 -1
View File
@@ -45,7 +45,19 @@ function exec(cmd) {
function detectWorkspacePath() {
const base = join(homedir(), '.openclaw')
// 1. Current agent-scoped layout
// 1. Read from openclaw.json (canonical source)
const configPath = join(base, 'openclaw.json')
if (existsSync(configPath)) {
try {
const config = JSON.parse(readFileSync(configPath, 'utf-8'))
const ws = config?.agents?.defaults?.workspace
if (typeof ws === 'string' && existsSync(ws)) return ws
} catch {
// Invalid JSON, fall through
}
}
// 2. Current agent-scoped layout
const agentPath = join(base, 'agents', 'main', 'workspace')
if (existsSync(agentPath)) return agentPath