Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const OPENCLAW_SUPPORTED_PROVIDER_TYPES: ProviderType[] = [
'openai-compatible',
'anthropic',
'moonshot',
'zai',
]

export function isOpenClawSupportedProviderType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const providerTypeEnum = z.enum([
'chatgpt-pro',
'github-copilot',
'qwen-code',
'zai',
])

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/browseros-agent/apps/agent/lib/browseros/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export class McpPortError extends Error {
* @public
*/
export async function getAgentServerUrl(): Promise<string> {
// In development, always use the configured server port directly
if (env.VITE_BROWSEROS_SERVER_PORT) {
return `http://127.0.0.1:${env.VITE_BROWSEROS_SERVER_PORT}`
}

const supportsUnifiedPort = await Capabilities.supports(
Feature.UNIFIED_PORT_SUPPORT,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5402,5 +5402,45 @@
"outputCost": 0
}
]
},
"zai": {
"name": "z.ai",
"api": "https://api.z.ai/api/coding/paas/v4",
"doc": "https://docs.z.ai/guides/llm/glm-4.6",
"models": [
{
"id": "glm-4.6",
"name": "GLM-4.6",
"contextWindow": 200000,
"maxOutput": 131072,
"supportsImages": true,
"supportsReasoning": true,
"supportsToolCall": true,
"inputCost": 0.6,
"outputCost": 2.2
},
{
"id": "glm-4.5",
"name": "GLM-4.5",
"contextWindow": 128000,
"maxOutput": 96000,
"supportsImages": true,
"supportsReasoning": true,
"supportsToolCall": true,
"inputCost": 0.6,
"outputCost": 2.2
},
{
"id": "glm-4.5-air",
"name": "GLM-4.5-Air",
"contextWindow": 128000,
"maxOutput": 96000,
"supportsImages": true,
"supportsReasoning": true,
"supportsToolCall": true,
"inputCost": 0.2,
"outputCost": 1.1
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
OpenAI,
OpenRouter,
Qwen,
ZAI,
} from '@lobehub/icons'
import { Bot, Github } from 'lucide-react'
import type { FC, SVGProps } from 'react'
Expand Down Expand Up @@ -36,6 +37,7 @@ const providerIconMap: Record<ProviderType, IconComponent | null> = {
'chatgpt-pro': OpenAI,
'github-copilot': Github,
'qwen-code': Qwen,
zai: ZAI,
}

interface ProviderIconProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ export const providerTemplates: ProviderTemplate[] = [
setupGuideUrl:
'https://docs.aws.amazon.com/bedrock/latest/userguide/getting-started.html',
}),
{
id: 'zai',
name: 'z.ai',
defaultBaseUrl: 'https://api.z.ai/api/coding/paas/v4',
defaultModelId: 'glm-4.6',
supportsImages: true,
contextWindow: 200000,
apiKeyUrl: 'https://z.ai/manage-apikey/apikey-list',
setupGuideUrl: 'https://docs.z.ai/guides/llm/glm-4.6',
},
]

/**
Expand All @@ -161,6 +171,7 @@ export const providerTypeOptions: { value: ProviderType; label: string }[] = [
{ value: 'lmstudio', label: 'LM Studio' },
{ value: 'bedrock', label: 'AWS Bedrock' },
{ value: 'browseros', label: 'BrowserOS' },
{ value: 'zai', label: 'z.ai' },
]

/**
Expand Down Expand Up @@ -192,6 +203,7 @@ export const DEFAULT_BASE_URLS: Record<ProviderType, string> = {
lmstudio: 'http://localhost:1234/v1',
bedrock: '',
browseros: '',
zai: 'https://api.z.ai/api/coding/paas/v4',
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type ProviderType =
| 'chatgpt-pro'
| 'github-copilot'
| 'qwen-code'
| 'zai'

/**
* LLM Provider configuration
Expand Down
1 change: 0 additions & 1 deletion packages/browseros-agent/apps/agent/web-ext.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const chromiumArgs = [
'--use-mock-keychain',
'--show-component-extension-options',
'--disable-browseros-server',
'--disable-browseros-extensions',
'--browseros-dock-icon=dev',
]

Expand Down
12 changes: 12 additions & 0 deletions packages/browseros-agent/apps/server/src/agent/provider-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ function createMoonshotFactory(
})
}

function createZaiFactory(
config: ResolvedAgentConfig,
): (modelId: string) => unknown {
if (!config.apiKey) throw new Error('z.ai provider requires apiKey')
return createOpenAICompatible({
name: 'zai',
baseURL: config.baseUrl || EXTERNAL_URLS.ZAI_API,
apiKey: config.apiKey,
})
}

function createQwenCodeFactory(
config: ResolvedAgentConfig,
): (modelId: string) => unknown {
Expand Down Expand Up @@ -218,6 +229,7 @@ const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
[LLM_PROVIDERS.CHATGPT_PRO]: createChatGPTProFactory,
[LLM_PROVIDERS.GITHUB_COPILOT]: createGitHubCopilotFactory,
[LLM_PROVIDERS.QWEN_CODE]: createQwenCodeFactory,
[LLM_PROVIDERS.ZAI]: createZaiFactory,
}

export function createLanguageModel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export function createProviderRoutes(deps: ProviderRouteDeps = {}) {
model: config.model,
})

logger.info('Testing provider connection start', {
provider: config.provider,
model: config.model,
baseUrl: config.baseUrl,
hasApiKey: !!config.apiKey,
})

const result = await testProviderConnection(config, deps.browserosId)

logger.info('Provider test result', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const SUPPORTED_OPENCLAW_PROVIDERS = [
'openai',
'anthropic',
'moonshot',
'zai',
] as const

export type SupportedOpenClawProvider =
Expand All @@ -32,6 +33,7 @@ const PROVIDER_ENV_VARS: Record<SupportedOpenClawProvider, string> = {
moonshot: 'MOONSHOT_API_KEY',
openai: 'OPENAI_API_KEY',
openrouter: 'OPENROUTER_API_KEY',
zai: 'ZAI_API_KEY',
}

export class UnsupportedOpenClawProviderError extends Error {
Expand Down
41 changes: 37 additions & 4 deletions packages/browseros-agent/apps/server/src/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,43 @@ export class Browser {
// --- Pages ---

async listPages(): Promise<PageInfo[]> {
const result = await this.cdp.Browser.getTabs({ includeHidden: true })
const tabs = (result.tabs as TabInfo[]).filter(
(t) => !EXCLUDED_URL_PREFIXES.some((prefix) => t.url.startsWith(prefix)),
)
let tabs: TabInfo[]
try {
const result = await this.cdp.Browser.getTabs({ includeHidden: true })
tabs = (result.tabs as TabInfo[]).filter(
(t) =>
!EXCLUDED_URL_PREFIXES.some((prefix) => t.url.startsWith(prefix)),
)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
// Fallback for CDP implementations without Browser.getTabs (e.g. some
// Chromium forks or older builds).
if (msg.includes("wasn't found") || msg.includes('not found')) {
const result = await this.cdp.Target.getTargets()
tabs = result.targetInfos
.filter((t) => t.type === 'page')
.map((t) => ({
tabId: t.tabId ?? 0,
targetId: t.targetId,
url: t.url,
title: t.title,
isActive: false,
isLoading: false,
loadProgress: 1,
isPinned: false,
isHidden: false,
windowId: t.windowId,
index: 0,
groupId: undefined,
}))
.filter(
(t) =>
!EXCLUDED_URL_PREFIXES.some((prefix) => t.url.startsWith(prefix)),
)
} else {
throw err
}
}

const seenTargetIds = new Set<string>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ function createMoonshotModel(config: ResolvedLLMConfig): LanguageModel {
})(config.model)
}

function createZaiModel(config: ResolvedLLMConfig): LanguageModel {
logger.info('createZaiModel', {
model: config.model,
baseUrl: config.baseUrl || EXTERNAL_URLS.ZAI_API,
hasApiKey: !!config.apiKey,
})
if (!config.apiKey) throw new Error('z.ai provider requires apiKey')
return createOpenAICompatible({
name: 'zai',
baseURL: config.baseUrl || EXTERNAL_URLS.ZAI_API,
apiKey: config.apiKey,
})(config.model)
}

function createQwenCodeModel(config: ResolvedLLMConfig): LanguageModel {
if (!config.apiKey) throw new Error('Qwen Code requires OAuth authentication')
return createOpenAICompatible({
Expand Down Expand Up @@ -196,6 +210,7 @@ const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
[LLM_PROVIDERS.CHATGPT_PRO]: createChatGPTProModel,
[LLM_PROVIDERS.GITHUB_COPILOT]: createGitHubCopilotModel,
[LLM_PROVIDERS.QWEN_CODE]: createQwenCodeModel,
[LLM_PROVIDERS.ZAI]: createZaiModel,
}

export function createLLMProvider(config: ResolvedLLMConfig): LanguageModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

import { TIMEOUTS } from '@browseros/shared/constants/timeouts'
import type { LLMConfig } from '@browseros/shared/schemas/llm'
import { streamText } from 'ai'
import { generateText } from 'ai'
import { logger } from '../../logger'
import { resolveLLMConfig } from './config'
import { createLLMProvider } from './provider'

Expand All @@ -30,18 +31,34 @@ export async function testProviderConnection(
const startTime = performance.now()

try {
logger.info('testProviderConnection start', {
provider: config.provider,
model: config.model,
baseUrl: config.baseUrl,
hasApiKey: !!config.apiKey,
})
const resolvedConfig = await resolveLLMConfig(config, browserosId)
logger.info('testProviderConnection resolved', {
provider: resolvedConfig.provider,
model: resolvedConfig.model,
baseUrl: resolvedConfig.baseUrl,
})
const model = createLLMProvider(resolvedConfig)
logger.info('testProviderConnection model created', {
provider: resolvedConfig.provider,
})

// streamText works for all providers including Codex (which requires streaming)
const stream = streamText({
// Use generateText for testing to get clear API errors (streamText wraps
// APICallError in NoOutputGeneratedError and loses responseBody details).
const result = await generateText({
model,
messages: [{ role: 'user', content: TEST_PROMPT }],
maxRetries: 0,
abortSignal: AbortSignal.timeout(TIMEOUTS.TEST_PROVIDER),
})
const text = await stream.text
const responseTime = Math.round(performance.now() - startTime)

const text = result.text
if (text) {
const preview = text.length > 100 ? `${text.slice(0, 100)}...` : text
return {
Expand All @@ -58,7 +75,13 @@ export async function testProviderConnection(
}
} catch (error) {
const responseTime = Math.round(performance.now() - startTime)
const errorMessage = error instanceof Error ? error.message : String(error)
logger.info('testProviderConnection caught error', {
provider: config.provider,
errorType: typeof error,
errorName: error instanceof Error ? error.name : undefined,
errorMessage: error instanceof Error ? error.message : String(error),
})
const errorMessage = extractProviderErrorMessage(error, config.provider)

return {
success: false,
Expand All @@ -67,3 +90,37 @@ export async function testProviderConnection(
}
}
}

function extractProviderErrorMessage(
error: unknown,
_provider: string,
): string {
// Check for API call error with response body (generateText preserves
// APICallError directly, so responseBody is available on the error object)
if (
error != null &&
typeof error === 'object' &&
'responseBody' in error &&
typeof (error as { responseBody?: string }).responseBody === 'string'
) {
try {
const parsed = JSON.parse(
(error as { responseBody: string }).responseBody,
)
const msg =
parsed?.error?.message ||
parsed?.message ||
parsed?.error?.code ||
(error instanceof Error ? error.message : String(error))
return msg
} catch {
// Not valid JSON, fall through
}
}

if (error instanceof Error) {
return error.message
}

return String(error)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export const EXTERNAL_URLS = {
QWEN_DEVICE_CODE: 'https://chat.qwen.ai/api/v1/oauth2/device/code',
QWEN_OAUTH_TOKEN: 'https://chat.qwen.ai/api/v1/oauth2/token',
QWEN_CODE_API: 'https://portal.qwen.ai/v1',
ZAI_API: 'https://api.z.ai/api/coding/paas/v4',
} as const
Loading
Loading