Add two new image generation providers:
- `openai` — uses the standalone OpenAI Images API
(`/v1/images/generations`) with an API key. Supports DALL-E
and gpt-image-* models, with automatic parameter adjustment
(gpt-image models don't accept response_format or n).
- `openai_codex` — uses the Codex Responses API with the
`image_generation` tool, authenticated via OAuth subscription
token. The same mechanism ChatGPT uses internally.
Also remove the API key pre-check in ImageGenerationTool so
providers that handle their own auth fallback (like Codex OAuth)
can work without a configured key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the legacy unified-diff patch parameter and all related parsing/
generation logic (_parse_patch, _generate_patch, _apply_hunks, etc.).
The tool now accepts only the structured `edits` array, eliminating the
intermediate diff-string round-trip.
Also update file_edit_events tracking and tests to work exclusively
with edits.
Benchmark (zhipu glm-5.1, edits mode): 15/15 cases passed.
Moonshot's API rejects requests that carry both 'reasoning_effort'
(top-level kwarg) and 'thinking' (extra_body) at the same time.
After the unified thinking-style injection loop injects the native
'thinking' param for kimi models, pop 'reasoning_effort' from kwargs
since it is redundant and causes a 400 error.
Uses _model_slug() + _KIMI_THINKING_MODELS lookup to stay consistent
with the refactored code (the old _is_kimi_thinking_model helper was
removed in 4f895e63).
Existing kimi tests updated to assert 'reasoning_effort' is absent.
Xiaomi MiMo models are unaffected — their API accepts both params.
Closes#3939
Follow-up to #3851: that PR added `extra_body.thinking={type: disabled}`
for MiMo via OpenRouter, but OR doesn't forward provider-specific
thinking shapes to upstream — it strips unknown extra_body fields and
uses its own unified `reasoning` parameter. So MiMo via OR kept
thinking despite the injection (reproduced by @ClearPlume on #3851
with identical kwargs but provider switched from openrouter → xiaomi_mimo).
For known thinking-capable models (Kimi, MiMo) routed via the
openrouter spec, also inject `extra_body.reasoning = {effort: <effort>}`
in OR's documented enum ("none"|"minimal"|"low"|"medium"|"high"|"xhigh").
OR translates this to the upstream model's native shape.
Existing tests updated to expect both fields on the OR path. The direct
xiaomi_mimo and moonshot paths are unchanged (the new branch is gated
on spec.name == "openrouter"). Flash and non-MiMo models on OR continue
to receive no injection.
- Remove suppress(Exception) from poll loop and message processing; add
logger.exception so inbound errors are visible.
- Check both ret and errcode on send to avoid silent drops when iLink
returns ret != 0 with errcode == 0.
- Proactively refresh context_token via getconfig before sending if the
cached token is older than 60s. This prevents message loss on long
agent turns and cron pushes without relying on complex retry logic.
Refs: openclaw/openclaw#61174, NousResearch/hermes-agent#21011
Override _handle_message to publish directly to the bus for messages
that have already passed _check_inbound_policy. The denied DM pairing
path calls super()._handle_message() to issue pairing codes via the
base class. This avoids cross-policy leakage where e.g. group open
policy would cause is_allowed to incorrectly allow denied DM senders.
Also includes:
- SSE: strip one optional leading space after 'data:' per spec
- Convert 20+ f-string log calls to loguru lazy formatting
- Add end-to-end tests for DM/group routing through the full chain
- Add cross-policy test (dm allowlist + group open) for pairing
- Add Signal channel documentation to docs/chat-apps.md
BaseChannel.is_allowed ORs is_approved (the pairing store) into the
allow decision; the signal override dropped that step and only looked
at config.allow_from. With the new DM-pairing flow in place, an
approved-via-pairing sender's next message would have failed the
allow check and triggered another pairing code in a loop.
OR in a normalized check against the pairing store: walk each part of
the pipe-joined sender_id through _normalize_signal_id and call
is_approved for each variant, so an approval stored under one form
(phone with/without "+", UUID/ACI) still matches when the next inbound
uses a different form. Mirrors how slack.py:643 handles it.
Also tightens the empty-allowlist warning to only fire when nothing
else granted access, since pairing-store hits are now a valid path.
Not part of the original review, but Comments 2 and 3 turn this latent
gap into a broken round-trip — included so the pairing UX actually
works.
Per the SSE spec, multiple data: lines within a single event must be
joined with \n before parsing. signal-cli emits single-line JSON so
this was latent, but the joining was wrong.
Addresses review comment on PR #3852.
Previously _check_inbound_policy returned (False, chat_id) for DMs
that failed the allowlist and the caller dropped them — so unapproved
DM senders never saw a pairing code. Mirror Slack: when the policy
gate denies a DM but dm.enabled is true, still call
_handle_message(content="", is_dm=True) so BaseChannel can issue the
pairing reply. Group denials stay a hard drop.
Combined with the previous is_dm forwarding, unapproved DM senders
now receive a pairing code through the standard flow.
Addresses review comment on PR #3852.
BaseChannel._handle_message uses is_dm to decide whether to issue a
pairing code when is_allowed rejects the sender. Without it the base
class treats every denied message as a group message and silently
drops it. Forward is_dm=not is_group_message so unapproved DM users
get a pairing code through the standard flow.
This change only takes effect once denied DMs actually reach
_handle_message (next commit); on its own it is a no-op since the
policy gate still short-circuits before this call.
Addresses review comment on PR #3852.
_send_http_request collapses every exception path into a {"error": ...}
dict, so the if "error" in response branch inside send() is the only
place where send failures surface. Logging-only there meant the
ChannelManager retry mechanism never fired. Raise RuntimeError so the
base-class retry path is exercised; the outer try/except already
re-raises into the caller.
Addresses review comment on PR #3852.
The base BaseChannel.is_allowed() does a literal ``sender_id in allow_from``
check, but Signal's sender_id is a pipe-joined composite of phone/UUID
parts. After splitting an allowlist entry like ``+phone|uuid`` into two
separate entries, the per-DM gate accepted it but the base gate still
denied because the composite sender string wasn't literally in the list.
Override is_allowed on SignalChannel to delegate to
_sender_matches_allowlist, which already splits both sides on ``|`` and
normalizes each part. _sender_matches_allowlist itself now also splits
allowlist entries on ``|`` so legacy composite entries keep working too.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The existing markdown suite was strong on UTF-16 offsets and chunk
redistribution but had no coverage for nested or adjacent styles, no test
that an unmatched opener round-trips as plain text, and no test for the
blockquote/inline-code interaction. Add six cases including the
documented contiguous-BOLD output for `# **wrap** me`, which Signal
renders as one visual span.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two test classes (TestHandleDataMessageDM, TestHandleDataMessageGroup)
plus three TestCommandHandling tests each repeated the same handful of
lines: build a channel, mock _handle_message to record kwargs, replace
_start_typing with a no-op, paper over the assignment with type: ignore.
Hoist the pattern into _make_channel_with_capture and call it from all
five sites. Drops 30+ lines of duplication and 7 type: ignore comments.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously the SSE loop and the empty-phone-number short-circuit in start()
had zero coverage. Both now have tests: a fake httpx stream feeds canned
SSE lines, exercising the valid-frame, invalid-JSON, non-200, and
no-http-client paths; start() with an empty phone number is asserted to
return without entering the HTTP loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The receive-path handler was ~165 lines deep into nested DM/group policy
checks, buffer mutations, mention stripping, attachment downloads, and
final bus forwarding. Pull the policy gate out into _check_inbound_policy
(returns (allow, chat_id), still appends to the group buffer once allowed)
and the text+media construction into _assemble_inbound_content. The
top-level method now reads as orchestration only.
Add TestCheckInboundPolicy that exercises the helper directly across the
DM/group policy permutations, including the buffer side effect, so the
new seam is locked in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The inbound attachment loop hardcoded ~/.local/share/signal-cli/attachments
as the source path. That is the daemon's default on Linux but not on macOS
or Windows, and breaks if the daemon was launched with XDG_DATA_HOME set.
Add SignalConfig.attachments_dir as an optional override. When unset the
behavior is unchanged; when set the value is run through Path.expanduser()
so ~ is honored.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the inline try/except at the end of _handle_receive_notification
with a small async context manager that swallows the exception, logs
self.logger.error with the offending payload's repr (bounded to 200 chars),
and attaches the traceback via logger.opt(exception=True).
The previous log line only carried `e`, so diagnosing a bad envelope from
production logs required correlating timestamps. The wrapper is generic so
future receive/dispatch sites can adopt it; for now only this site uses it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The DM allowlist check split sender_id on '|' and looked for raw membership
in the allow_from list. Senders carry their phone number with a leading
'+' but admins routinely write allowlist entries without it (or vice
versa), and UUID/ACI matches were case-sensitive. Both forms now flow
through _normalize_signal_id, so an entry like 19995550001 matches a
sender +19995550001 and a UUID matches case-insensitively.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Hoist the cell-strip patterns to module level so they match the rest of
the module's regex style and aren't reparsed on every call.
- Type the markdown transform callback and the mention id walker so the
inline Callable signature is no longer an untyped Any.
- Add _HTTP_TIMEOUT_SECONDS alongside the other class-level tunables.
- Reject group_message_buffer_size <= 0 in a Pydantic field_validator
rather than silently disabling the buffer at write time.
- Mark SignalConfig.allow_from as a computed_field so it shows up in
model_dump() instead of being invisible to serialization.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>