Skip to content

feat(setup): separate env namespace for custom OpenAI-compat provider - #3248

Open
TheArchitectit wants to merge 770 commits into
ultraworkers:mainfrom
TheArchitectit:feat/custom-openai-namespace-upstream
Open

TheArchitectit wants to merge 770 commits into
ultraworkers:mainfrom
TheArchitectit:feat/custom-openai-namespace-upstream

Conversation

@TheArchitectit

Copy link
Copy Markdown
Contributor

Problem

The /setup wizard's "Custom (OpenAI-compat)" option saved kind: "openai" and injected OPENAI_API_KEY / OPENAI_BASE_URL. That collides with users who already have real OpenAI, NeuralWatt, or other platform credentials in their environment, so the saved custom proxy URL was ignored and requests were misrouted.

Changes

New provider kind: custom-openai

  • Uses dedicated env vars:
    • CLAWCUSTOMOPENAI_API_KEY
    • CLAWCUSTOMOPENAI_BASE_URL
  • New model routing prefix custom/ selects the OpenAI-compatible client with those env vars and is stripped on the wire, so the proxy receives the bare model id.

/setup wizard updated

  • Option 5 now saves kind: "custom-openai".
  • Prompts for CLAWCUSTOMOPENAI_API_KEY / CLAWCUSTOMOPENAI_BASE_URL.
  • Bare model names saved by /setup are normalized to custom/<model>.

Saved settings are applied at startup

  • inject_config_as_env_fallbacks() is called once in run() before any runtime threads are spawned.
  • Preserves 3-tier precedence: env var > .env file > stored config.

API routing

  • metadata_for_model and detect_provider_kind recognize custom/.
  • OpenAiCompatConfig::custom_openai() reads the new env vars.
  • wire_model_for_base_url strips custom/ prefix.

Tests

  • custom/ prefix routes to CLAWCUSTOMOPENAI_* env vars.
  • custom/ is stripped on the wire.
  • inject_config_as_env_fallbacks sets both standard and custom env vars.
  • Bare custom-openai model names normalize to custom/.

Docs

  • Updated USAGE.md provider matrix and prefix-routing section.
  • Added /setup wizard section explaining the custom provider.

Verification

  • cargo test -p api passes.
  • cargo test -p rusty-claude-cli --bin claw config_model passes.
  • cargo test -p rusty-claude-cli --bin claw inject_config passes.
  • Manual one-shot prompt against http://100.96.49.42:4001/v1 with model openclaw_3750 succeeded.

Notes

  • This PR intentionally does not include the TUI work; it targets upstream/main where the TUI changes are not present.
  • Existing users who previously saved kind: "openai" with a custom base URL can re-run /setup to migrate to the new namespace.

🤖 Generated with Claude Code
Co-Authored-By: Claude Fable 5 noreply@anthropic.com

code-yeongyu and others added 30 commits May 27, 2026 09:06
…ath_is_directory kind + hint; wire fallback_hint_for_error_kind into both resume error emission sites
…rror envelope; use exit(1) instead of Err propagation
…pty success; now returns unknown_option error
…lugin_source_not_found instead of unknown+null
…now return non-null hints via fallback table
…ng not-found instead of unexpected_extra_args
…limited usage string

Parity with ultraworkers#791 (config extra-arg fix). The plugins arg parser emitted
'unexpected extra arguments after claw plugins show ...' with no newline
delimiter, so split_error_hint returned None. Added usage hint after newline.
60 CLI contract tests pass.
…ed usage string

claw '' and claw '   ' returned empty_prompt + hint:null because the
error message had no newline delimiter. Added usage hint. 61 CLI
contract tests pass.
…tion classifier arms

Two classifier arms had no corresponding assert_eq! in
test_classify_error_kind_returns_correct_discriminants: invalid_history_count
(both prefix and contains paths) and unknown_option (ultraworkers#790). Now 49/39 = full
coverage of all classify_error_kind return values.
…(plugins extra-arg, empty-prompt, classifier coverage)
… returned wrong error instead of unexpected_extra_args
…ltraworkers#3161)

Keep malformed diff invocations with trailing JSON format flags on the parser error path and lock the contract with focused output-format regressions.

Constraint: Do not touch tracked .omx state files.

Rejected: Repeating direct binary smoke loops | local auth/provider configuration intercepts those invocations and obscures parser behavior.

Confidence: high

Scope-risk: narrow

Tested: git diff --check; cargo fmt --check; cargo test -p rusty-claude-cli diff_extra_args_have_typed_error_kind_and_hint_766 --test output_format_contract; cargo test -p rusty-claude-cli diff_trailing_json_after_malformed_args_is_bounded_json_3129 --test output_format_contract; cargo test -p rusty-claude-cli diff_non_git_dir_has_error_kind_and_hint_801 --test output_format_contract
…ailures

Extend auto-compaction error detection to handle additional error patterns
from llama.cpp backends: 'Context size has been exceeded',
'exceed_context_size_error', 'exceeds the available context size'. Also
recover from reqwest 'error decoding response body' errors — some
llama.cpp instances return a non-SSE plaintext HTTP 500 on context overflow,
causing the SSE deserializer to fail.

Add dynamic threshold adaptation: parse server-reported context window
size from error messages (e.g., '(81920 tokens)') and set the auto-
compaction trigger at 70% of that value. This replaces the need for a
hardcoded threshold, adapting automatically to any backend's limits.

This patch was developed with assistance from OpenCode and local Qwen 3.6
API server.
@1716775457damn

Copy link
Copy Markdown

Good separation of concerns — having a dedicated env namespace avoids collisions with standard OpenAI vars. The migration path for existing users looks clean. Thanks @TheArchitectit!

@TheArchitectit

Copy link
Copy Markdown
Contributor Author

Ready for review ✅

  • Rebased and mergeable (CLEAN state, no conflicts)
  • Not a draft
  • All commits are self-contained to the custom OpenAI-compat env namespace feature

Once this lands, #3250 will be rebased to drop commits 1-3 and should be ready to merge as well.

@ultraworkers/maintainers — requesting review when you get a chance.

@1716775457damn

Copy link
Copy Markdown

The custom-openai namespace separation is clean and well-tested. The custom/ prefix routing is a nice touch that keeps model IDs unambiguous. The migration path for existing users (re-run /setup) is documented. All checks passing, good to merge.

@1716775457damn

Copy link
Copy Markdown

Rebased cleanly and the custom-openai namespace approach is solid. All checks green — merging. Thanks for the thorough work @TheArchitectit!

@TheArchitectit

Copy link
Copy Markdown
Contributor Author

Thank you! Once this merges I'll start work on the follow-up PR (#3250) — I'll rebase it to drop the first three commits as you noted. More PRs to come as well!

@1716775457damn

Copy link
Copy Markdown

Merged, thanks! Looking forward to #3250 — the custom-openai namespace work is a solid foundation. Will keep an eye on the follow-up PRs for review.

@1716775457damn

Copy link
Copy Markdown

Separating env namespace for custom OpenAI-compatible providers is a clean design choice. This avoids collision with the built-in OpenAI provider config and makes the setup more predictable.

procaffe121 and others added 6 commits July 31, 2026 17:08
No behavior change. Move the inline probe out of
unshare_user_namespace_works into a reusable unshare_probe helper and a
cached working_unshare_mapping() that picks the first working candidate
from UNSHARE_MAPPING_CANDIDATES, so the launcher and the capability probe
share one code path.
…icted

Plain `unshare --user --map-root-user` fails on kernels and containers
that block unprivileged writes to /proc/self/uid_map (e.g. GitHub Actions,
restricted AppArmor profiles). On those systems util-linux delegates to the
setuid newuidmap/newgidmap helpers when --map-auto is also present.

Add the combined form as a fallback candidate and build the launcher args
from the probed mapping, so systems without newuidmap/newgidmap or a
/etc/subuid range keep using the plain form.
… fallback

The fallback candidate relies on the setuid newuidmap/newgidmap helpers
(uidmap package) plus a subuid/subgid range for the current user. Note in
the candidate docs that the startup probe rejects the candidate when those
are missing, so the plain --map-root-user form is used instead.
The startup probe validated only the mapping flags against the trivial
program `true`, but the real launcher always adds
--mount --ipc --pid --uts --fork. On environments where the user
namespace is created but mount propagation inside it is restricted
(e.g. AppArmor-restricted CI runners), the fallback mapping passed the
probe and the sandbox activated, yet every sandboxed command died with
"cannot change root filesystem propagation: Permission denied",
silently returning empty tool output and breaking the mock parity
suite.

The candidates now define the complete static launcher shape (mapping
flags + namespace flags), so probe success implies launch success; the
launcher reuses the candidate instead of re-appending the namespace
flags, keeping probe and launch as one source of truth. The
order-guarding test asserts the namespace flags are present in every
candidate.

Co-authored-by: linkst <2024023709@m.scnu.edu.cn>
…ap-auto-fallback

fix(sandbox): fall back to --map-auto when root-user mapping is restricted
Root knowledge base plus complexity-scored subdirectory files for the
rust/ workspace, its five highest-mass crates (runtime, rusty-claude-cli,
api, tools, commands, plugins), and the src/ Python porting workspace.

Generated via init-deep: 13 parallel explore agents, LSP/ast-grep code
map, centrality-scored placement. Snapshot in .omo/init-deep.json (local).
@TheArchitectit

Copy link
Copy Markdown
Contributor Author

Friendly ping — this is a small, self-contained change (3 commits, 6 files): a dedicated CLAWCUSTOMOPENAI_* env namespace for private OpenAI-compatible endpoints, plus /setup wizard support for it.

It has been CLEAN/mergeable for a while, and PR #3250 (team enhancements) is queued on top of it — its final review note says "ready once #3248 lands and the base commits are rebased away."

Happy to rebase onto current main if that helps move it along — just say the word.

TheArchitectit and others added 3 commits September 8, 2026 07:23
The /setup wizard saves apiKey and baseUrl to ~/.claw/settings.json,
but the API client constructors (OpenAiCompatClient::from_env,
AnthropicClient::from_env) only read environment variables. This caused
saved provider settings to be silently ignored — you'd run /setup,
set a custom URL and API key, and the runtime would still try to use
the default endpoint.

Now AnthropicRuntimeClient::new() calls inject_config_as_env_fallbacks()
before constructing the API client. This function loads the config file's
provider settings and sets the corresponding env vars (OPENAI_API_KEY,
OPENAI_BASE_URL, etc.) only when they aren't already set — preserving
the 3-tier resolution order: env var > .env file > stored config.

This is a process-level env injection (set_var), so it only affects
the current claw process and its children, not the parent shell.
…oints

- Move config-to-env injection out of AnthropicRuntimeClient::new so
  parallel unit tests are not affected by global env mutations.
- Call inject_config_as_env_fallbacks() once at binary startup in run(),
  preserving the env-var > .env > stored-config precedence.
- Normalize bare model names (e.g. openclaw) to openai/openclaw when a
  custom OpenAI-compatible base URL is configured, so validation passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Custom (OpenAI-compat) /setup option was saving kind: openai and
injecting OPENAI_API_KEY / OPENAI_BASE_URL. That collides with users who
have real OpenAI/NeuralWatt credentials in their environment.

Introduce a dedicated custom-openai provider kind that uses its own
environment variables:

- CLAWCUSTOMOPENAI_API_KEY
- CLAWCUSTOMOPENAI_BASE_URL

A new custom/ routing prefix selects the OpenAI-compatible client with
those env vars and is stripped on the wire, so the proxy receives the
bare model id. /setup now saves kind: custom-openai and prompts for the
new env vars. Bare model names saved by /setup are normalized to
custom/<model>.

Manual verification against http://100.96.49.42:4001/v1 succeeds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
@TheArchitectit
TheArchitectit force-pushed the feat/custom-openai-namespace-upstream branch from 0c339e7 to c4cbd2b Compare September 8, 2026 15:10
@1716775457damn

Copy link
Copy Markdown

Namespace separation looks good — dedicated CLAWCUSTOMOPENAI_* vars cleanly avoid colliding with real OpenAI credentials. One suggestion: the migration path currently relies on users re-running /setup. Could we print a deprecation warning at startup when an old saved config (kind=openai with custom base_url) is detected, instead of silently ignoring it? That way users whose proxy URL was being ignored get a clear hint they need to migrate.

@TheArchitectit

Copy link
Copy Markdown
Contributor Author

Thanks — the namespace separation review is appreciated. On the deprecation warning: we dug in, and the framing is slightly off. kind=openai with a custom baseUrl isn't deprecated (and we don't plan to deprecate it) — it's still loaded and injected via inject_config_as_env_fallbacks() into OPENAI_API_KEY/OPENAI_BASE_URL, including the documented proxy case where a custom OPENAI_BASE_URL is deliberate (USAGE.md).

The real silent failure is shadowing: the std::env::var(base_url_env).is_err() guards (~line 12697 on the PR head) skip the stored proxy baseUrl whenever real OPENAI_* env vars already exist, so the saved model never gets custom/ routing. That's the user who "re-runs /setup with no hint."

A blanket "deprecated config" warning would false-positive on the deliberate proxy setups. We'll land a narrower warning instead: fire only when a stored config value is actually ignored because env already differs, once per process at startup, suppressed in JSON/quiet mode. Will link the follow-up here when it's up.

@1716775457damn

Copy link
Copy Markdown

明白——CLAUCCUSTOMOPENAI_* 命名空间与旧 kind=openai 兼容方案并存是合理的,不会被当作"降级"。我会更新评审结论:baseUrl 仍支持注入 fallback,#3250 合并后无需额外弃用警告。该 PR 自包含且 CLEAN,可以合。

@TheArchitectit

Copy link
Copy Markdown
Contributor Author

Thanks for the quick follow-up and for clarifying the namespace decision — good to know the CLAUCCUSTOMOPENAI_* namespace is fine to coexist with the legacy kind=openai compatibility scheme, with no deprecation warning needed after #3250 lands. Appreciate you confirming the baseUrl fallback-injection behavior as well. Thanks for the clean, self-contained review — much appreciated! 🙏

@1716775457damn

Copy link
Copy Markdown

收到,确认可以合并。这个命名空间方案我会在合并后的运行时回归里把 CLAUCCUSTOMOPENAI_* 与旧 kind=openai 共存的场景一并覆盖,确保 fallback 注入行为不回归。#3250 等你 rebase 掉前三个 commit 后提上来,我继续跟进评审。

@1716775457damn

Copy link
Copy Markdown

Merged — thanks for clarifying the shadowing behavior. Good to know the real silent-failure case is the std::env::var guards skipping the stored proxy baseUrl whenever real OPENAI_* vars exist, not the namespacing itself. I've added that coexistence scenario (CLAWCUSTOMOPENAI_* alongside legacy kind=openai) to the post-merge regression coverage so the fallback-injection behavior stays intact. With this in, #3250's rebase is now unblocked — I'll continue that review.

@1716775457damn

Copy link
Copy Markdown

合并完成,感谢确认。落地的回归覆盖我会具体断言三点:① fallback 注入顺序保持 env var > .env > stored config 不变;② 影子场景——存在真实 OPENAI_* 环境变量时,stored model 仍能经 CLAWCUSTOMOPENAI_* 正确路由到 custom/ 前缀;③ 与旧 kind=openai 共存时互不污染。跑通后我把结果贴到 #3250 的评审里。

@TheArchitectit

Copy link
Copy Markdown
Contributor Author

Quick status check on this one, since it directly gates #3250.

As of now this PR is still open: state=open, merged=false, merged_at=null (mergeable_state clean). main has not moved — HEAD is 08106b0 (2026-08-16T06:18Z), zero commits on main since 2026-09-01, and the repo's most recent merge is #3280 on 2026-08-06. CLAWCUSTOMOPENAI resolves to 0 matches on main, versus 27 matches across 5 files on feat/custom-openai-namespace-upstream.

So the merge-order block is still physically in place, and the test restore for #3250 can't be validated yet: config_model_normalizes_bare_name_to_custom_prefix_for_custom_openai_provider exists only on this branch, and the custom/ normalization it covers (is_custom_openai_provider plus the bare-name → custom/ route in config_model_for_current_dir) is in neither main nor #3250. Re-adding it on #3250 today would fail cargo test --workspace.

Meanwhile #3250 is already in the shape that was asked for: the 8 team commits only, rebased onto current main (0 behind), MERGEABLE/CLEAN, all six checks green (build, clippy, fmt, test, docs source-of-truth, windows smoke), with the model_token_limit catch-all → None fix present (2de2aa59).

I pre-ran the #3250 rebase locally against a simulated main + this branch. The conflict surface is small — 5 hunks, all in rust/crates/rusty-claude-cli/src/main.rs:

  1. inject_config_as_env_fallbacks() call site — local fn vs runtime::
  2. removal of the local inject_config_as_env_fallbacks fn body
  3. the custom/-normalization test block
  4. two test call sites — super:: vs runtime::

Resolution is mechanical: take #3250's runtime::-qualified side, and keep the restored test from this branch. rust/crates/api/src/providers/mod.rs auto-merges cleanly.

Proposed order: merge this PR → rebase #3250 onto the new main, resolve those hunks and confirm the restored test passes → push to TheArchitectit:feat/team-pr-clean → CI green → final head-to-head.

If the merge landed somewhere other than ultraworkers/claw-code:main (another fork or a mirror I can't see), point me at the ref and I'll rebase against it straight away.

@1716775457damn

Copy link
Copy Markdown

合并目标就是 ultraworkers/claw-code:main,没有合并到其他 fork。你提出的顺序(先合 #3248 → rebase #3250 → CI 绿)没问题,按这个推进即可。

@1716775457damn

Copy link
Copy Markdown

收到收尾确认:custom-openai 命名空间与 legacy kind=openai 兼容方案并存合理,无需额外弃用警告;narrower warning(仅当 env 已存在导致存储值被忽略时触发)也值得在 #3250 之后跟进落地。本 PR 已合并,期待 #3250。

@TheArchitectit
TheArchitectit force-pushed the feat/custom-openai-namespace-upstream branch from c4cbd2b to eafa950 Compare September 29, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.