diff --git a/docs/CUSTOM-PROVIDERS.md b/docs/CUSTOM-PROVIDERS.md index 3e1afa9a0..dedcdb60d 100644 --- a/docs/CUSTOM-PROVIDERS.md +++ b/docs/CUSTOM-PROVIDERS.md @@ -107,6 +107,7 @@ Required fields for custom providers: - `ANTHROPIC_AUTH_TOKEN` is used instead of `ANTHROPIC_API_KEY` — this is the z.ai API key - The `API_TIMEOUT_MS` env var extends the request timeout (z.ai can be slower than direct Anthropic) - If you get auth errors, run `/logout` inside Claude Code before switching to the z.ai provider +- Web search (`WebSearch` tool) is an Anthropic-only server-side feature — third-party endpoints don't support it. Add `"disallowedTools": ["WebSearch"]` to avoid errors. - Automated setup is also available: `npx @z_ai/coding-helper` - Official docs: [docs.z.ai/devpack/tool/claude](https://docs.z.ai/devpack/tool/claude) @@ -173,6 +174,7 @@ For pay-as-you-go, use `ANTHROPIC_API_KEY` with a standard Model Studio key (`sk - API keys must be created in the **Singapore region** - The coding plan is for personal use only in interactive coding tools +- Web search (`WebSearch` tool) is an Anthropic-only server-side feature — third-party endpoints don't support it. Add `"disallowedTools": ["WebSearch"]` to avoid errors. - Official docs: [alibabacloud.com/help/en/model-studio/claude-code-coding-plan](https://www.alibabacloud.com/help/en/model-studio/claude-code-coding-plan) --- @@ -436,6 +438,7 @@ Every entry under `agents.providers` accepts these fields: | `command` | `string[]` | Yes (ACP only) | Command to spawn the agent process | | `env` | `Record` | No | Environment variables to set for the agent process | | `models` | `ProviderProfileModel[]` | No | Static model list (overrides runtime discovery) | +| `disallowedTools` | `string[]` | No | Tool names to disable for this provider (e.g. `["WebSearch"]`) | | `enabled` | `boolean` | No | Set to `false` to hide the provider (default: `true`) | | `order` | `number` | No | Sort order in the provider list | @@ -460,6 +463,29 @@ Each entry in the `models` array: | `description` | `string` | No | Short description | | `isDefault` | `boolean` | No | Mark as the default thinking option | +### Gotcha: `extends: "claude"` with third-party endpoints + +When a custom provider extends `"claude"` but points `ANTHROPIC_BASE_URL` at a non-Anthropic API (Z.AI, Alibaba/Qwen, proxies), the Claude Agent SDK may try to use Anthropic-only server-side tools like `WebSearch`. Third-party APIs don't support these tools, causing errors. + +Use `disallowedTools` to disable unsupported tools: + +```json +{ + "agents": { + "providers": { + "my-proxy": { + "extends": "claude", + "label": "My Proxy", + "env": { + "ANTHROPIC_BASE_URL": "https://my-proxy.example.com/v1" + }, + "disallowedTools": ["WebSearch"] + } + } + } +} +``` + ### Valid `extends` values Built-in providers: `claude`, `codex`, `copilot`, `opencode`, `pi` diff --git a/packages/server/src/server/agent/provider-launch-config.ts b/packages/server/src/server/agent/provider-launch-config.ts index cf7de3a9b..e03ce335c 100644 --- a/packages/server/src/server/agent/provider-launch-config.ts +++ b/packages/server/src/server/agent/provider-launch-config.ts @@ -34,6 +34,7 @@ export const ProviderRuntimeSettingsSchema = z .object({ command: ProviderCommandSchema.optional(), env: z.record(z.string()).optional(), + disallowedTools: z.array(z.string()).optional(), }) .strict(); @@ -64,6 +65,7 @@ export const ProviderOverrideSchema = z command: z.array(z.string().min(1)).min(1).optional(), env: z.record(z.string()).optional(), models: z.array(ProviderProfileModelSchema).optional(), + disallowedTools: z.array(z.string()).optional(), enabled: z.boolean().optional(), order: z.number().optional(), }) diff --git a/packages/server/src/server/agent/provider-registry.test.ts b/packages/server/src/server/agent/provider-registry.test.ts index e19a087e7..8d669c1bd 100644 --- a/packages/server/src/server/agent/provider-registry.test.ts +++ b/packages/server/src/server/agent/provider-registry.test.ts @@ -473,6 +473,52 @@ describe("buildProviderRegistry", () => { expect(mockState.isCommandAvailable).toHaveBeenCalledWith("claude"); }); + test("disallowedTools flows through to runtime settings", () => { + buildProviderRegistry(logger, { + providerOverrides: { + claude: { + disallowedTools: ["WebSearch", "WebFetch"], + }, + }, + }); + + expect(mockState.constructorArgs.claude[0]).toEqual({ + runtimeSettings: { + command: undefined, + env: undefined, + disallowedTools: ["WebSearch", "WebFetch"], + }, + }); + }); + + test("derived provider inherits and merges disallowedTools from base", () => { + buildProviderRegistry(logger, { + providerOverrides: { + claude: { + disallowedTools: ["WebSearch"], + }, + zai: { + extends: "claude", + label: "ZAI", + disallowedTools: ["ComputerUse"], + }, + }, + }); + + const zaiArgs = mockState.constructorArgs.claude.find( + (entry) => + Array.isArray((entry.runtimeSettings as { disallowedTools?: string[] })?.disallowedTools) && + (entry.runtimeSettings as { disallowedTools: string[] }).disallowedTools.includes( + "ComputerUse", + ), + ); + expect(zaiArgs).toBeDefined(); + expect((zaiArgs!.runtimeSettings as { disallowedTools: string[] }).disallowedTools).toEqual([ + "WebSearch", + "ComputerUse", + ]); + }); + test("extension inherits base override — override claude command, zai extends claude gets overridden command", () => { buildProviderRegistry(logger, { providerOverrides: { diff --git a/packages/server/src/server/agent/provider-registry.ts b/packages/server/src/server/agent/provider-registry.ts index 0707a8534..a7d383078 100644 --- a/packages/server/src/server/agent/provider-registry.ts +++ b/packages/server/src/server/agent/provider-registry.ts @@ -90,7 +90,7 @@ function getProviderClientFactory(provider: string): ProviderClientFactory { } function toRuntimeSettings(override?: ProviderOverride): ProviderRuntimeSettings | undefined { - if (!override?.command && !override?.env) { + if (!override?.command && !override?.env && !override?.disallowedTools) { return undefined; } @@ -102,6 +102,7 @@ function toRuntimeSettings(override?: ProviderOverride): ProviderRuntimeSettings } : undefined, env: override.env, + disallowedTools: override.disallowedTools, }; } @@ -122,6 +123,10 @@ function mergeRuntimeSettings( ...(override?.env ?? {}), } : undefined, + disallowedTools: + base?.disallowedTools || override?.disallowedTools + ? [...(base?.disallowedTools ?? []), ...(override?.disallowedTools ?? [])] + : undefined, }; } diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 9ce93275a..90b137654 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -2145,6 +2145,12 @@ class ClaudeAgentSession implements AgentSession { if (this.claudeSessionId) { base.resume = this.claudeSessionId; } + if (this.runtimeSettings?.disallowedTools?.length) { + base.disallowedTools = [ + ...(base.disallowedTools ?? []), + ...this.runtimeSettings.disallowedTools, + ]; + } return this.applyRuntimeSettings(base); }