#How do I configure OpenClaw to route specific model calls through a local OpenAI-compatible proxy

30 messages · Page 1 of 1 (latest)

wary field
#

How do I configure OpenClaw to route specific model calls through a local OpenAI-compatible proxy (claude-max-api-proxy on localhost:3456) without overriding the global OPENAI_BASE_URL for all providers?

I want openai/claude-sonnet-4 and openai/claude-opus-4 to hit http://localhost:3456/v1 while openai/gpt-5.4 continues to hit OpenAI directly.

Also: is there a supported way to use the local claude CLI (authenticated via Max plan OAuth) as an ACP agent target, instead of the @agentclientprotocol/claude-agent-acp npm package which requires API credits?

pine inletBOT
opaque windBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

opal magnetBOT
#

Exec approval is required, but native chat exec approvals are not configured on Discord.

Approve it from the Web UI or terminal UI for now. Discord supports native exec approvals for this account. Configure channels.discord.execApprovals.approvers or commands.ownerAllowFrom; leave channels.discord.execApprovals.enabled unset/auto or set it to true.

#

Let me verify the docs/source so I don’t make up config keys.

opal magnetBOT
# wary field How do I configure OpenClaw to route specific model calls through a local OpenAI...

Short version:

  1. No, not under the same openai/* provider namespace.
    OPENAI_BASE_URL is provider-scoped, so if you override it for openai, it affects all openai/* models.
    For selective routing, the supported path is a separate custom provider id, for example claude-max/*.

  2. Use a custom provider for the proxy, keep openai/gpt-5.4 on native OpenAI.

{
  env: {
    OPENAI_API_KEY: "${OPENAI_API_KEY}",
  },

  agents: {
    defaults: {
#
      model: { primary: "openai/gpt-5.4" },
      models: {
        "openai/gpt-5.4": {},
        "claude-max/claude-sonnet-4": { alias: "sonnet-max" },
        "claude-max/claude-opus-4": { alias: "opus-max" },
      },
    },
  },

  models: {
    mode: "merge",
    providers: {
      "claude-max": {
        baseUrl: "http://127.0.0.1:3456/v1",
        apiKey: "not-needed",
#
        api: "openai-completions",
        models: [
          {
            id: "claude-sonnet-4",
            name: "Claude Sonnet 4 via Max proxy",
            reasoning: false,
            input: ["text"],
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
            contextWindow: 120000,
            maxTokens: 8192,
          },
          {
            id: "claude-opus-4",
            name: "Claude Opus 4 via Max proxy",
            reasoning: false,
#
            input: ["text"],
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
            contextWindow: 120000,
            maxTokens: 8192,
          },
        ],
      },
    },
  },
}

A couple of notes:

  • Leave OPENAI_BASE_URL unset if you want native OpenAI to stay native.
  • The actual refs become claude-max/claude-sonnet-4 and claude-max/claude-opus-4, not openai/claude-*.
#
  • If the proxy is picky, you may also need:
    • compat.requiresStringContent: true
    • compat.supportsTools: false
  1. ACP answer: I do not think there is a supported “point ACP directly at my arbitrary local claude binary instead of the Claude ACP adapter” mode right now.
    The supported split is:
  • ACP harness path: claude via ACP adapter stack, for full ACP sessions
  • CLI backend path: claude-cli/*, if you want direct local Claude CLI reuse, but that is not ACP

So if your goal is Max-plan local Claude auth without Anthropic API keys, the documented supported lane today is closer to CLI backends, not a raw custom ACP target.

Docs:

#

If you want, paste openclaw --version and I can turn this into an exact config snippet for your current config layout.

wary field
#

OpenClaw 2026.4.8 (9ece252)

opal magnetBOT
#

Thanks, that helps.

On 2026.4.8, I’d still do it this way:

1) Do not use OPENAI_BASE_URL

That env var is effectively provider-wide for openai/*, so it won’t give you “some openai/* direct, some proxied”.

2) Add a separate custom provider for the proxy

Use a different provider id, for example claude-max/*, and leave openai/gpt-5.4 on native OpenAI.

{
  env: {
    OPENAI_API_KEY: "${OPENAI_API_KEY}",
  },
#
  agents: {
    defaults: {
      model: {
        primary: "openai/gpt-5.4",
        // optional:
        // fallbacks: ["claude-max/claude-sonnet-4", "claude-max/claude-opus-4"],
      },
      models: {
        "openai/gpt-5.4": { alias: "gpt" },
        "claude-max/claude-sonnet-4": { alias: "sonnet-max" },
        "claude-max/claude-opus-4": { alias: "opus-max" },
      },
    },
  },
#
  models: {
    mode: "merge",
    providers: {
      "claude-max": {
        baseUrl: "http://127.0.0.1:3456/v1",
        apiKey: "not-needed",
        api: "openai-completions",
        models: [
          {
            id: "claude-sonnet-4",
            name: "Claude Sonnet 4 via Max proxy",
            reasoning: false,
            input: ["text"],
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
            contextWindow: 120000,
#
            maxTokens: 8192,
          },
          {
            id: "claude-opus-4",
            name: "Claude Opus 4 via Max proxy",
            reasoning: false,
            input: ["text"],
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
            contextWindow: 120000,
            maxTokens: 8192,
          },
        ],
      },
    },
  },
#
}

A couple of practical notes:

  • api: "openai-completions" is the important bit here, since claude-max-api-proxy exposes /v1/chat/completions, not native OpenAI Responses.
  • The refs become:
    • openai/gpt-5.4 → direct OpenAI
    • claude-max/claude-sonnet-4 → localhost proxy
    • claude-max/claude-opus-4 → localhost proxy
  • If the proxy complains about message content shape, add:
    • compat: { requiresStringContent: true }
  • If tool calling is flaky through the proxy, add:
    • compat: { supportsTools: false }

3) About keeping the names openai/claude-*

#

I don’t think that’s a supported split. The provider prefix is the routing boundary, so if you need different base URLs, use different provider ids.

4) ACP + local Claude CLI

For ACP, I do not think there’s a supported “use my raw local claude binary as the ACP target instead of the Claude ACP adapter” mode on 2026.4.8.

The supported lanes are basically:

  • ACP session: Claude via the ACP adapter stack
  • CLI backend: claude-cli/* for direct local Claude CLI reuse

So if your real goal is Max-plan OAuth through the local Claude CLI, the documented/supported route is CLI backend, but that is not ACP.

Docs:

#

If you want, I can turn this into a drop-in snippet for your existing openclaw.json if you paste the current agents.defaults + models sections.

#

Nice, glad it’s fixed 🎉

If this solved it, please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow can index it for the next person.

wary field
#

On 2026.4.8 with a custom claude-max provider pointing at claude-max-api-proxy on localhost:3456, subagent tasks arrive at Claude as [object Object] instead of the actual task string. Krill suggested compat.requiresStringContent: true but that key is rejected as unrecognized in models.providers.claude-max. Where does requiresStringContent go in the config on this version?

opal magnetBOT
#

Yep, on 2026.4.8 it goes on the individual model entry, not the provider root.

So this is wrong:

models: {
  providers: {
    "claude-max": {
      compat: { requiresStringContent: true }, // wrong place
    },
  },
}

This is the right shape:

#
{
  models: {
    mode: "merge",
    providers: {
      "claude-max": {
        baseUrl: "http://127.0.0.1:3456/v1",
        apiKey: "not-needed",
        api: "openai-completions",
        models: [
          {
            id: "claude-sonnet-4",
            name: "Claude Sonnet 4 via Max proxy",
            reasoning: false,
            input: ["text"],
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
#
            contextWindow: 120000,
            maxTokens: 8192,
            compat: {
              requiresStringContent: true,
            },
          },
          {
            id: "claude-opus-4",
            name: "Claude Opus 4 via Max proxy",
            reasoning: false,
            input: ["text"],
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
            contextWindow: 120000,
            maxTokens: 8192,
            compat: {
#
              requiresStringContent: true,
            },
          },
        ],
      },
    },
  },
}

So the path is:

models.providers.<provider>.models[].compat.requiresStringContent

not:

wary field
#

On 2026.4.8 with a custom claude-max provider pointing at claude-max-api-proxy on localhost:3456, subagent tasks arrive at Claude as [object Object] instead of the actual task string. Krill suggested compat.requiresStringContent: true but that key is rejected as unrecognized in models.providers.claude-max. Where does requiresStringContent go in the config on this version?

opal magnetBOT
#

requiresStringContent belongs on the model entry inside models.providers.<provider>.models[], not on the provider itself.

So for 2026.4.8, use:

models: {
  providers: {
    "claude-max": {
      baseUrl: "http://127.0.0.1:3456/v1",
      apiKey: "not-needed",
      api: "openai-completions",
      models: [
        {
          id: "claude-sonnet-4",
          name: "Claude Sonnet 4 via Max proxy",
          reasoning: false,
#
          input: ["text"],
          cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
          contextWindow: 120000,
          maxTokens: 8192,
          compat: {
            requiresStringContent: true,
          },
        },
      ],
    },
  },
}

So the path is: