Compare commits

...

1 Commits

Author SHA1 Message Date
Nikhil Sonti
cc40b2c133 fix(openclaw): normalize openrouter model slug and stream gateway logs in dev
OpenRouter's public model slugs use dots in version numbers
(e.g. `anthropic/claude-haiku-4.5`), but openclaw's model registry only
recognises the dashed form (`claude-haiku-4-5`). Passing the dotted form
makes openclaw's registry lookup miss silently — the agent turn completes
with `stopReason=stop payloads=0` and the UI shows no reply. Rewrite dots
to dashes in the model portion for openrouter providers only so
copy-pasted OpenRouter slugs resolve correctly.

Also, in development mode:
  - Inject `logging.level: debug` into generated openclaw.json so the
    gateway emits debug-level entries to its file log.
  - Patch an existing openclaw.json on start/restart so already-provisioned
    users pick up the debug setting without a reset.
  - Tail the gateway container's logs into the browseros server logger so
    they appear in the same stream as the rest of dev output.
2026-04-15 12:48:01 -07:00
4 changed files with 113 additions and 2 deletions

View File

@@ -141,6 +141,13 @@ export class ContainerRuntime {
)
}
tailGatewayLogs(onLine: LogFn): () => void {
return this.podman.tailContainerLogs(
OPENCLAW_GATEWAY_CONTAINER_NAME,
onLine,
)
}
private async compose(args: string[], onLog?: LogFn): Promise<number> {
const lines: string[] = []
const code = await this.podman.runCommand(['compose', ...args], {

View File

@@ -65,6 +65,21 @@ function hasBuiltinProvider(providerType?: string): providerType is string {
return !!providerType && providerType in PROVIDER_ENV_MAP
}
/**
* OpenRouter's public slugs use dots for version numbers
* (e.g. `anthropic/claude-haiku-4.5`), but openclaw's model registry expects
* dashes (`claude-haiku-4-5`). Passing the dotted form makes openclaw fail
* the registry lookup silently and the agent turn completes with zero
* payloads. Rewrite dots to dashes for openrouter model ids only.
*/
function normalizeBuiltinModelId(
providerType: string,
modelId: string,
): string {
if (providerType !== 'openrouter') return modelId
return modelId.replace(/\./g, '-')
}
export function deriveOpenClawProviderId(providerInput: {
providerType?: string
providerName?: string
@@ -102,10 +117,14 @@ export function resolveProviderConfig(
providerKeys[PROVIDER_ENV_MAP[input.providerType]] = input.apiKey
}
const normalizedModelId = input.modelId
? normalizeBuiltinModelId(input.providerType, input.modelId)
: undefined
return {
providerKeys,
model: input.modelId
? `${input.providerType}/${input.modelId}`
model: normalizedModelId
? `${input.providerType}/${normalizedModelId}`
: undefined,
}
}
@@ -222,6 +241,10 @@ export function buildBootstrapConfig(
config.models = provider.models
}
if (process.env.NODE_ENV === 'development') {
config.logging = { level: 'debug', consoleLevel: 'debug' }
}
return config
}

View File

@@ -116,6 +116,7 @@ export class OpenClawService {
private lastGatewayError: string | null = null
private lastRecoveryReason: OpenClawGatewayRecoveryReason | null = null
private gatewayReconnectPromise: Promise<void> | null = null
private stopLogTail: (() => void) | null = null
constructor(browserosServerPort?: number) {
this.openclawDir = getOpenClawDir()
@@ -174,6 +175,7 @@ export class OpenClawService {
logProgress('Starting OpenClaw gateway...')
await this.runtime.composeUp(logProgress)
this.startGatewayLogTail()
logProgress('Waiting for gateway readiness...')
const ready = await this.runtime.waitForReady(this.port, READY_TIMEOUT_MS)
@@ -218,9 +220,11 @@ export class OpenClawService {
logProgress('Loading gateway auth token...')
await this.loadTokenFromEnv()
await this.ensureDevLoggingInConfig()
await this.runtime.ensureReady(logProgress)
logProgress('Starting OpenClaw gateway...')
await this.runtime.composeUp(logProgress)
this.startGatewayLogTail()
logProgress('Waiting for gateway readiness...')
const ready = await this.runtime.waitForReady(this.port, READY_TIMEOUT_MS)
@@ -237,6 +241,7 @@ export class OpenClawService {
async stop(): Promise<void> {
this.disconnectGateway()
this.stopGatewayLogTail()
await this.runtime.composeStop()
logger.info('OpenClaw container stopped')
}
@@ -245,10 +250,13 @@ export class OpenClawService {
const logProgress = this.createProgressLogger(onLog)
this.disconnectGateway()
this.stopGatewayLogTail()
logProgress('Loading gateway auth token...')
await this.loadTokenFromEnv()
await this.ensureDevLoggingInConfig()
logProgress('Restarting OpenClaw gateway...')
await this.runtime.composeRestart(logProgress)
this.startGatewayLogTail()
logProgress('Waiting for gateway readiness...')
const ready = await this.runtime.waitForReady(this.port, READY_TIMEOUT_MS)
@@ -287,6 +295,7 @@ export class OpenClawService {
async shutdown(): Promise<void> {
this.disconnectGateway()
this.stopGatewayLogTail()
try {
await this.runtime.composeStop()
} catch {
@@ -797,6 +806,52 @@ export class OpenClawService {
await writeFile(configPath, JSON.stringify(config, null, 2))
}
private async ensureDevLoggingInConfig(): Promise<void> {
if (process.env.NODE_ENV !== 'development') return
const configPath = join(this.openclawDir, OPENCLAW_CONFIG_FILE)
if (!existsSync(configPath)) return
try {
const raw = await readFile(configPath, 'utf-8')
const parsed = JSON.parse(raw) as Record<string, unknown>
const existing = (parsed.logging ?? {}) as Record<string, unknown>
if (existing.level === 'debug' && existing.consoleLevel === 'debug') {
return
}
parsed.logging = { ...existing, level: 'debug', consoleLevel: 'debug' }
await writeFile(configPath, JSON.stringify(parsed, null, 2))
logger.info('Patched openclaw.json for dev debug logging')
} catch (err) {
logger.warn('Failed to patch openclaw.json for dev debug logging', {
error: err instanceof Error ? err.message : String(err),
})
}
}
private startGatewayLogTail(): void {
if (process.env.NODE_ENV !== 'development') return
if (this.stopLogTail) return
try {
this.stopLogTail = this.runtime.tailGatewayLogs((line) => {
logger.debug(`[openclaw] ${line}`)
})
logger.info('Streaming OpenClaw gateway logs into server log (dev mode)')
} catch (err) {
logger.warn('Failed to start OpenClaw gateway log tail', {
error: err instanceof Error ? err.message : String(err),
})
}
}
private stopGatewayLogTail(): void {
if (!this.stopLogTail) return
try {
this.stopLogTail()
} catch {
// best effort
}
this.stopLogTail = null
}
private getHostWorkspaceDir(agentName: string): string {
return join(
this.openclawDir,

View File

@@ -162,6 +162,32 @@ export class PodmanRuntime {
return proc.exited
}
/**
* Follow container logs. Returns a stop function that terminates the
* underlying `podman logs -f` process. Each output line is passed to
* onLine as-is.
*/
tailContainerLogs(containerName: string, onLine: LogFn): () => void {
const proc = Bun.spawn(
[this.podmanPath, 'logs', '-f', '--tail', '0', containerName],
{ stdout: 'pipe', stderr: 'pipe' },
)
void this.drainStream(proc.stdout ?? null, onLine)
void this.drainStream(proc.stderr ?? null, onLine)
let stopped = false
return () => {
if (stopped) return
stopped = true
try {
proc.kill()
} catch {
// process may already be gone
}
}
}
/**
* Lists running container names. Used to check whether non-BrowserOS
* containers are running before stopping the Podman machine.