- Remove retry_after: None from ApiError::Api structs in openai_compat.rs (field was removed)
- Remove SlashCommand::Team parse arm (variant was removed from enum)
- Add config_load_error_kind: None to doctor path StatusContext initializer
- Add Thinking arm to all ContentBlock match blocks in trident.rs
- Remove cargo fmt drift across commands, config, compact, tools, trident
Adds early return in wire_model_for_base_url for Gemini/Gemma/XAI/Kimi/Grok model prefixes to ensure the provider prefix is preserved correctly when routing through the OpenAI-compatible provider path.
Changes the catch-all arm in model_token_limit() from None to conservative defaults (max_output_tokens: 16_384, context_window_tokens: 131_072) to prevent crashes when an unknown model is used.
Sets user_agent on both build_http_client_or_default() and build_http_client_with() to 'clawd-rust-tools/0.1' for consistent HTTP client identification.
Resolve the G012 evidence gate by fixing permission-mode regressions, platform-sensitive tests, and the clippy surface that blocked an all-targets verification run.
Constraint: G012 final gate required docs, board, full workspace tests, and clippy -D warnings evidence before checkpointing.
Rejected: documenting the worker-2 gate failure as an accepted gap | the failing tests and lints were locally reproducible and fixable.
Confidence: high
Scope-risk: moderate
Directive: Preserve read-only permission requirements for read/glob/grep tools; write/edit remain workspace-write or danger-full-access when outside the workspace.
Tested: python3 .github/scripts/check_doc_source_of_truth.py; python3 .github/scripts/check_release_readiness.py; python3 scripts/validate_cc2_board.py --board .omx/cc2/board.json; python3 .omx/cc2/validate_issue_parity_intake.py .omx/cc2/issue-parity-intake.json; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml --workspace; cargo test --manifest-path rust/Cargo.toml --workspace -- --nocapture; cargo clippy --manifest-path rust/Cargo.toml --workspace --all-targets -- -D warnings
Not-tested: live network provider smoke tests and remote PR/issue mutations.
Keep the G008 capability and diagnostic helpers compile-ready by restoring the public report/support/severity types that team integrations referenced after merge reconciliation.
Constraint: Final G008 verification failed on missing provider capability and diagnostic type definitions.
Confidence: high
Scope-risk: narrow
Directive: Keep provider diagnostics exported as typed API surfaces; do not replace them with ad-hoc JSON-only status fields.
Tested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; git diff --check; cargo test --manifest-path rust/Cargo.toml -p api providers:: -- --nocapture --test-threads=1; cargo test --manifest-path rust/Cargo.toml -p api --test openai_compat_integration -- --nocapture --test-threads=1
Not-tested: full workspace clippy; known unrelated runtime policy_engine struct_excessive_bools remains outside G008.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Keep the mock HTTP/SSE/proxy coverage deterministic under strict linting while preserving provider request behavior.\n\nConstraint: Task 4 scope is limited to OpenAI-compatible HTTP/SSE/proxy coverage and provider compatibility surfaces.\nRejected: Environment-variable proxy testing | It races with parallel integration tests and can route unrelated localhost mocks through a single proxy fixture.\nConfidence: high\nScope-risk: narrow\nDirective: Prefer explicit injected reqwest clients for proxy integration tests instead of mutating process proxy environment.\nTested: cargo fmt --check; cargo check -p api; cargo test -p api --test openai_compat_integration -- --nocapture; cargo test -p api\nNot-tested: cargo clippy --no-deps -p api --all-targets -- -D warnings fails on pre-existing anthropic.rs/providers/mod.rs lints outside task scope.\n\nCo-authored-by: OmX <omx@local>
Keep integrated G008 provider changes formatted and compile-ready so worker follow-up commits can merge against a clean leader baseline.
Constraint: G008 provider verification must pass before ultragoal checkpointing.
Confidence: high
Scope-risk: narrow
Directive: Keep provider compatibility follow-ups rebased on this formatted baseline before retrying failed cherry-picks.
Tested: cargo test --manifest-path rust/Cargo.toml -p api providers:: -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p api --test openai_compat_integration -- --nocapture --test-threads=1
Not-tested: full workspace clippy; known pre-existing runtime policy_engine LaneContext clippy warning remains outside this change.
Co-authored-by: OmX <omx@oh-my-codex.dev>
Five interrelated fixes from parallel Hephaestus sessions:
1. fix(repl): display assistant text after spinner (#2981, #2982, #2937)
- Added final_assistant_text() call after run_turn spinner completes
- REPL now shows response text like run_prompt_json does
2. fix(compact): handle Thinking content blocks (#2985)
- Added ContentBlock::Thinking variant throughout compact summarizer
- Prevents panic when /compact encounters thinking blocks
3. fix(prompt): provider-aware model identity (#2822)
- New ModelFamilyIdentity enum (Claude vs Generic)
- Non-Anthropic models no longer say 'I am Claude'
- model_family_identity_for() detects provider and sets identity
4. fix(openai): preserve DeepSeek reasoning_content (#2821)
- Stream parser now captures reasoning_content from OpenAI-compat
- Emits ThinkingDelta/SignatureDelta events for reasoning models
- Thinking blocks included in conversation history for re-send
5. feat(runtime): Thinking block support across codebase
- AssistantEvent::Thinking variant in conversation.rs
- ContentBlock::Thinking in session serialization
- Thinking-aware compact summarization
- Tests for thinking block ordering and content
Closes#2981, #2982, #2937, #2985, #2822, #2821
Run rustfmt from the Rust workspace so CI format checks pass without changing behavior.
Constraint: Scope is formatting-only across tracked Rust files
Confidence: high
Scope-risk: narrow
Tested: cd rust && cargo fmt --check
Tested: git diff --check
Add ModelTokenLimit entries for kimi-k2.5 and kimi-k1.5 to enable
preflight context window validation. Per Moonshot AI documentation:
- Context window: 256,000 tokens
- Max output: 16,384 tokens
Includes 3 unit tests:
- returns_context_window_metadata_for_kimi_models
- kimi_alias_resolves_to_kimi_k25_token_limits
- preflight_blocks_oversized_requests_for_kimi_models
All tests pass, clippy clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add "kimi" to the strip_routing_prefix matches so that models like
"kimi/kimi-k2.5" have their prefix stripped before sending to the
DashScope API (consistent with qwen/openai/xai/grok handling).
Also add unit test strip_routing_prefix_strips_kimi_provider_prefix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Changes in rust/crates/api/src/providers/mod.rs:
- Add 'kimi' alias to MODEL_REGISTRY resolving to 'kimi-k2.5' with DashScope config
- Add kimi/kimi- prefix routing to DashScope endpoint in metadata_for_model()
- Add resolve_model_alias() handling for kimi -> kimi-k2.5
- Add unit tests: kimi_prefix_routes_to_dashscope, kimi_alias_resolves_to_kimi_k2_5
Users can now use:
- --model kimi (resolves to kimi-k2.5)
- --model kimi-k2.5 (auto-routes to DashScope)
- --model kimi/kimi-k2.5 (explicit provider prefix)
All 127 tests pass, clippy clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add structured error context to API failures:
- Request ID tracking across retries with full context in error messages
- Provider-specific error code mapping with actionable suggestions
- Suggested user actions for common error types (401, 403, 413, 429, 500, 502-504)
- Added suggested_action field to ApiError::Api variant
- Updated enrich_bearer_auth_error to preserve suggested_action
Files changed:
- rust/crates/api/src/error.rs: Add suggested_action field, update Display
- rust/crates/api/src/providers/openai_compat.rs: Add suggested_action_for_status()
- rust/crates/api/src/providers/anthropic.rs: Update error handling
All tests pass, clippy clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added criterion benchmarks and optimized flatten_tool_result_content:
- Added criterion dev-dependency and request_building benchmark suite
- Optimized flatten_tool_result_content to pre-allocate capacity and avoid
intermediate Vec construction (was collecting to Vec then joining)
- Made key functions public for benchmarking: translate_message,
build_chat_completion_request, flatten_tool_result_content,
is_reasoning_model, model_rejects_is_error_field
Benchmark results:
- flatten_tool_result_content/single_text: ~17ns
- translate_message/text_only: ~200ns
- build_chat_completion_request/10 messages: ~16.4µs
- is_reasoning_model detection: ~26-42ns
All 119 unit tests and 29 integration tests pass.
cargo clippy passes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added 4 unit tests to verify is_error field handling for kimi models:
- model_rejects_is_error_field_detects_kimi_models: Detects kimi-k2.5, kimi-k1.5, dashscope/kimi-k2.5 (case insensitive)
- translate_message_includes_is_error_for_non_kimi_models: Verifies gpt-4o, grok-3, claude include is_error
- translate_message_excludes_is_error_for_kimi_models: Verifies kimi models exclude is_error (prevents 400 Bad Request)
- build_chat_completion_request_kimi_vs_non_kimi_tool_results: Full integration test for request building
All 119 unit tests and 29 integration tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ROADMAP #37 was still open even though several earlier backlog items were
already closed. This change removes the local login/logout surface, stops
startup auth resolution from treating saved OAuth credentials as a supported
path, and updates diagnostics/help to point users at ANTHROPIC_API_KEY or
ANTHROPIC_AUTH_TOKEN only.
While proving the change with the user-requested workspace gates, clippy
surfaced additional pre-existing warning failures across the Rust workspace.
Those were cleaned up in-place so the required `cargo fmt`, `cargo clippy
--workspace --all-targets -- -D warnings`, and `cargo test --workspace`
sequence now passes end to end.
Constraint: User explicitly required full-workspace fmt/clippy/test before commit/push
Constraint: Existing dirty leader worktree had to be stashed before attempted OMX team worktree launch
Rejected: Keep login/logout but hide them from help | left unsupported auth flow and saved OAuth fallback intact
Rejected: Stop after ROADMAP #37 targeted tests | did not satisfy required full-workspace verification gate
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Do not reintroduce saved OAuth as a silent Anthropic startup fallback without an explicit supported auth policy
Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Remote push effects beyond origin/main update
When OPENAI_BASE_URL is set, the user explicitly configured an
OpenAI-compatible endpoint (Ollama, LM Studio, vLLM, etc.). Model names
like 'qwen2.5-coder:7b' or 'llama3:latest' don't match any recognized
prefix, so detect_provider_kind() fell through to Anthropic — asking for
Anthropic credentials even though the user clearly intended a local
provider.
Now: OPENAI_BASE_URL + OPENAI_API_KEY beats Anthropic env-check in the
cascade. OPENAI_BASE_URL alone (no API key — common for Ollama) is a
last-resort fallback before the Anthropic default.
Source: MaxDerVerpeilte in #claw-code (Ollama + qwen2.5-coder:7b);
traced by gaebal-gajae.
- Add #[allow(dead_code)] on test-only Delta struct (content field
used for deserialization but not read in assertion)
- Remove duplicated #[test] attribute on
assistant_message_without_tool_calls_omits_tool_calls_field
Zero warnings in cargo test --workspace.
Adds sanitize_tool_message_pairing() called from build_chat_completion_request()
after translate_message() runs. Drops any role:"tool" message whose
immediately-preceding non-tool message is role:"assistant" but has no
tool_calls entry matching the tool_call_id.
This is the second layer of the tool-pairing invariant defense:
- 6e301c8: compaction boundary fix (producer layer)
- this commit: request-builder sanitizer (sender layer)
Together these close the 400-error loop for resumed/compacted multi-turn
tool sessions on OpenAI-compatible backends.
Sanitization only fires when preceding message is role:assistant (not
user/system) to avoid dropping valid translation artifacts from mixed
user-message content blocks.
Regression tests: sanitize_drops_orphaned_tool_messages covers valid pair,
orphaned tool (no tool_calls in preceding assistant), mismatched id, and
two tool results both referencing the same assistant turn.
116 api + 159 CLI + 431 runtime tests pass. Fmt clean.
Same fix as ff416ff but for the streaming path. Some backends embed an
error JSON object in an SSE data: frame:
data: {"error":{"message":"context too long","code":400}}
parse_sse_frame() was attempting to deserialize this as ChatCompletionChunk
and failing with 'missing field' / 'invalid type', hiding the actual
backend error message.
Fix: check for an 'error' key before full chunk deserialization, same as
the non-streaming path in ff416ff. Symmetric pair:
- ff416ff: non-streaming path (response body)
- this: streaming path (SSE data: frame)
115 api + 159 CLI tests pass. Fmt clean.
When a local/proxy OpenAI-compatible backend returns an error object:
{"error":{"message":"...","type":"...","code":...}}
claw was trying to deserialize it as a ChatCompletionResponse and
failing with the cryptic 'failed to parse OpenAI response: missing
field id', completely hiding the actual backend error message.
Fix: before full deserialization, check if the parsed JSON has an
'error' key and promote it directly to ApiError::Api so the user
sees the real error (e.g. 'The number of tokens to keep from the
initial prompt is greater than the context length').
Source: devilayu in #claw-code 2026-04-09 — local LM Studio context
limit error was invisible; user saw 'missing field id' instead.
159 CLI + 115 api tests pass. Fmt clean.
When serializing a multi-turn conversation for the OpenAI-compatible path,
assistant messages with no tool calls were always emitting 'tool_calls: []'.
Some providers reject requests where a prior assistant turn carries an
explicit empty tool_calls array (400 on subsequent turns after a plain
text assistant response).
Fix: only include 'tool_calls' in the serialized assistant message when
the vec is non-empty. Empty case omits the field entirely.
This is a companion fix to fd7aade (null tool_calls in stream delta).
The two bugs are symmetric: fd7aade handled inbound null -> empty vec;
this handles outbound empty vec -> field omitted.
Two regression tests added:
- assistant_message_without_tool_calls_omits_tool_calls_field
- assistant_message_with_tool_calls_includes_tool_calls_field
115 api tests pass. Fmt clean.
Source: gaebal-gajae repro 2026-04-09 (400 on multi-turn, companion to
null tool_calls stream-delta fix).
Some OpenAI-compatible providers emit 'tool_calls: null' in streaming
delta chunks instead of omitting the field or using an empty array:
"delta": {"content":"","function_call":null,"tool_calls":null}
serde's #[serde(default)] only handles absent keys — an explicit null
value still fails deserialization with:
'invalid type: null, expected a sequence'
Fix: replace #[serde(default)] with a custom deserializer helper
deserialize_null_as_empty_vec() that maps null -> Vec::default(),
keeping the existing absent-key default behaviour.
Regression test added: delta_with_null_tool_calls_deserializes_as_empty_vec
uses the exact provider response shape from gaebal-gajae's repro (2026-04-09).
112 api lib tests pass. Fmt clean.
Companion to gaebal-gajae's local 448cf2c — independently reproduced
and landed on main.
gpt-5.x models reject requests with max_tokens and require max_completion_tokens.
Detect wire model starting with 'gpt-5' and switch the JSON key accordingly.
Older models (gpt-4o etc.) continue to receive max_tokens unchanged.
Two regression tests added:
- gpt5_uses_max_completion_tokens_not_max_tokens
- non_gpt5_uses_max_tokens
140 api tests pass, cargo fmt clean.
Users of OpenAI-compatible reasoning models (o4-mini, o3, deepseek-r1,
etc.) had no way to control reasoning effort — the field was missing from
MessageRequest and never emitted in the request body.
Changes:
- Add `reasoning_effort: Option<String>` to `MessageRequest` in types.rs
- Annotated with skip_serializing_if = "Option::is_none" for clean JSON
- Accepted values: "low", "medium", "high" (passed through verbatim)
- In `build_chat_completion_request`, emit `"reasoning_effort"` when set
- Two unit tests:
- `reasoning_effort_is_included_when_set`: o4-mini + "high" → field present
- `reasoning_effort_omitted_when_not_set`: gpt-4o, no field → absent
Existing callers use `..Default::default()` and are unaffected.
One struct-literal test that listed all fields explicitly updated with
`reasoning_effort: None`.
The CLI flag to expose this to users is a follow-up (ROADMAP #34 partial).
This commit lands the foundational API-layer plumbing needed for that.
Partial ROADMAP #34.