Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dd68547
fix(gemini): preserve thought_signature on tool_call round-trip for 3…
May 14, 2026
ef543c8
fix(gemini): 修正 v0.15.2 移植後直 thought_signature 持久化缺口
Jun 2, 2026
5695195
Add gateway verify code helper
May 29, 2026
54d5630
Intercept Discord verify owner code
May 29, 2026
4ccfc75
Intercept LINE verify owner code
May 29, 2026
f1509b3
Intercept Telegram verify owner code
May 29, 2026
037b5f0
Intercept WhatsApp verify owner code
May 29, 2026
afdf883
Intercept Slack verify owner code
May 29, 2026
6af03e2
Fix Telegram verify command handling
May 29, 2026
bd3d5d5
Fix WhatsApp verify group identity
May 29, 2026
9494834
test+impl: line_admin requirements gate
Jun 1, 2026
f4f1603
test+impl: line_admin constructor namespace
Jun 1, 2026
fbe7290
test+impl: line_admin platform registration
Jun 1, 2026
f85bb9e
test+impl: line_admin requirements combinations
Jun 1, 2026
856ce45
test+impl: line_admin hmac isolation
Jun 1, 2026
cb51cf8
test+impl: line_admin session namespace
Jun 1, 2026
daaaff5
test+impl: line_admin verify code platform
Jun 1, 2026
cfdb371
test+impl: line_admin optional env metadata
Jun 1, 2026
cfa88c2
test: keep line register assertions on customer entry
Jun 1, 2026
620ab1a
test+impl: line_admin media prefix
Jun 1, 2026
9e34b2c
test+impl: line_admin setup wizard
Jun 1, 2026
5182112
fix: line_admin credential env names
Jun 1, 2026
c2d3667
fix: line_admin lock namespace
Jun 1, 2026
f5d94e4
fix: line_admin health platform
Jun 1, 2026
caaacf4
Forward Discord roundtable keyword to SaaS
Jun 2, 2026
01644b9
fix: 修正 Discord 一對一免 mention 判斷
Jun 3, 2026
c5b0c4b
test: align telegram fixture with v0.15.2
Jun 4, 2026
c1ad874
fix: let Discord env mention policy override stale config
Jun 5, 2026
50dfa1e
fix: expose hermes quality gate scripts
Jun 28, 2026
2f33973
fix: harden bridge and hermetic checks
Jun 28, 2026
f258f11
docs: refresh hermes cloud gated review disposition
Jun 28, 2026
c3da150
docs: refresh hermes cloud preflight log
Jun 29, 2026
21bc6d2
fix: resolve hermes cloud dependency audit findings
Jun 30, 2026
dcf17cf
fix: require whatsapp bridge token
Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,13 +830,20 @@ def _read_claude_code_credentials_from_keychain() -> Optional[Dict[str, Any]]:
logger.debug("Keychain: no entry found for 'Claude Code-credentials'")
return None

raw = result.stdout.strip()
stdout = result.stdout
if isinstance(stdout, bytes):
stdout = stdout.decode("utf-8", errors="replace")
if not isinstance(stdout, str):
logger.debug("Keychain: credentials payload was not text")
return None

raw = stdout.strip()
if not raw:
return None

try:
data = json.loads(raw)
except json.JSONDecodeError:
except (json.JSONDecodeError, TypeError):
logger.debug("Keychain: credentials payload is not valid JSON")
return None

Expand Down
10 changes: 10 additions & 0 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,9 @@ def build_assistant_message(agent, assistant_message, finish_reason: str) -> dic
if hasattr(extra, "model_dump"):
extra = extra.model_dump()
tc_dict["extra_content"] = extra
sig = getattr(tool_call, "thought_signature", None)
if isinstance(sig, str) and sig:
tc_dict["thought_signature"] = sig
tool_calls.append(tc_dict)
msg["tool_calls"] = tool_calls

Expand Down Expand Up @@ -1851,6 +1854,7 @@ def _call_chat_completions():
"type": "function",
"function": {"name": "", "arguments": ""},
"extra_content": None,
"thought_signature": None,
}
entry = tool_calls_acc[idx]
if tc_delta.id:
Expand All @@ -1875,6 +1879,11 @@ def _call_chat_completions():
if hasattr(extra, "model_dump"):
extra = extra.model_dump()
entry["extra_content"] = extra
sig = getattr(tc_delta, "thought_signature", None)
if sig is None and hasattr(tc_delta, "model_extra"):
sig = (tc_delta.model_extra or {}).get("thought_signature")
if isinstance(sig, str) and sig:
entry["thought_signature"] = sig
# Fire once per tool when the full name is available
name = entry["function"]["name"]
if name and idx not in tool_gen_notified:
Expand Down Expand Up @@ -1929,6 +1938,7 @@ def _call_chat_completions():
id=tc["id"],
type=tc["type"],
extra_content=tc.get("extra_content"),
thought_signature=tc.get("thought_signature"),
function=SimpleNamespace(
name=tc["function"]["name"],
arguments=arguments,
Expand Down
26 changes: 20 additions & 6 deletions agent/gemini_cloudcode_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ def _coerce_content_to_text(content: Any) -> str:
return str(content)


def _tool_call_thought_signature(tool_call: Dict[str, Any]) -> Optional[str]:
direct = tool_call.get("thought_signature") or tool_call.get("thoughtSignature")
if isinstance(direct, str) and direct:
return direct

extra = tool_call.get("extra_content") or {}
if not isinstance(extra, dict):
return None
google = extra.get("google") or extra.get("thought_signature")
if isinstance(google, dict):
sig = google.get("thought_signature") or google.get("thoughtSignature")
return sig if isinstance(sig, str) and sig else None
return google if isinstance(google, str) and google else None


def _translate_tool_call_to_gemini(tool_call: Dict[str, Any]) -> Dict[str, Any]:
"""OpenAI tool_call -> Gemini functionCall part."""
fn = tool_call.get("function") or {}
Expand All @@ -93,16 +108,15 @@ def _translate_tool_call_to_gemini(tool_call: Dict[str, Any]) -> Dict[str, Any]:
args = {"_raw": args_raw}
if not isinstance(args, dict):
args = {"_value": args}
return {
part = {
"functionCall": {
"name": fn.get("name") or "",
"args": args,
},
# Sentinel signature — matches opencode-gemini-auth's approach.
# Without this, Code Assist rejects function calls that originated
# outside its own chain.
"thoughtSignature": "skip_thought_signature_validator",
}
}
thought_signature = _tool_call_thought_signature(tool_call)
part["thoughtSignature"] = thought_signature or "skip_thought_signature_validator"
return part


def _translate_tool_result_to_gemini(message: Dict[str, Any]) -> Dict[str, Any]:
Expand Down
4 changes: 4 additions & 0 deletions agent/gemini_native_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ def _extract_multimodal_parts(content: Any) -> List[Dict[str, Any]]:


def _tool_call_extra_signature(tool_call: Dict[str, Any]) -> Optional[str]:
direct = tool_call.get("thought_signature") or tool_call.get("thoughtSignature")
if isinstance(direct, str) and direct:
return direct

extra = tool_call.get("extra_content") or {}
if not isinstance(extra, dict):
return None
Expand Down
38 changes: 33 additions & 5 deletions agent/transports/chat_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"""

import copy
from typing import Any, Dict
from typing import Any, Dict, Optional

from agent.lmstudio_reasoning import resolve_lmstudio_effort
from agent.moonshot_schema import is_moonshot_model, sanitize_moonshot_tools
Expand Down Expand Up @@ -115,6 +115,22 @@ def _model_consumes_thought_signature(model: Any) -> bool:
return "gemini" in m or "gemma" in m


def _tool_call_thought_signature(tool_call: Any) -> Optional[str]:
for attr in ("thought_signature", "thoughtSignature"):
sig = getattr(tool_call, attr, None)
if isinstance(sig, str) and sig:
return sig

model_extra = getattr(tool_call, "model_extra", None) or {}
if isinstance(model_extra, dict):
for key in ("thought_signature", "thoughtSignature"):
sig = model_extra.get(key)
if isinstance(sig, str) and sig:
return sig

return None


class ChatCompletionsTransport(ProviderTransport):
"""Transport for api_mode='chat_completions'.

Expand All @@ -135,10 +151,10 @@ def convert_messages(
- Codex Responses API fields: ``codex_reasoning_items`` /
``codex_message_items`` on the message, ``call_id`` /
``response_item_id`` on ``tool_calls`` entries.
- ``extra_content`` on ``tool_calls`` (Gemini thought_signature) —
- ``extra_content`` / direct ``thought_signature`` on ``tool_calls`` —
stripped unless the outgoing ``model`` is itself Gemini-family.
Gemini 3 thinking models attach it for replay, but strict providers
(Fireworks, Mistral) reject any payload containing it with
Gemini 3 thinking models attach signatures for replay, but strict
providers (Fireworks, Mistral) reject any payload containing it with
``Extra inputs are not permitted, field: 'messages[N].tool_calls[M].extra_content'``.
It must be kept for Gemini targets (replay required) and dropped for
everyone else, including non-Gemini models that inherited stale
Expand Down Expand Up @@ -184,7 +200,14 @@ def convert_messages(
if isinstance(tc, dict) and (
"call_id" in tc
or "response_item_id" in tc
or (strip_extra_content and "extra_content" in tc)
or (
strip_extra_content
and (
"extra_content" in tc
or "thought_signature" in tc
or "thoughtSignature" in tc
)
)
):
needs_sanitize = True
break
Expand Down Expand Up @@ -214,6 +237,8 @@ def convert_messages(
tc.pop("response_item_id", None)
if strip_extra_content:
tc.pop("extra_content", None)
tc.pop("thought_signature", None)
tc.pop("thoughtSignature", None)
return sanitized

def convert_tools(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
Expand Down Expand Up @@ -628,6 +653,9 @@ def normalize_response(self, response: Any, **kwargs) -> NormalizedResponse:
except Exception:
pass
tc_provider_data["extra_content"] = extra
thought_signature = _tool_call_thought_signature(tc)
if thought_signature:
tc_provider_data["thought_signature"] = thought_signature
tool_calls.append(
ToolCall(
id=tc.id,
Expand Down
6 changes: 6 additions & 0 deletions agent/transports/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ def extra_content(self) -> dict[str, Any] | None:
"""
return (self.provider_data or {}).get("extra_content")

@property
def thought_signature(self) -> str | None:
"""Gemini OpenAI-compatible thought signature from provider_data."""
sig = (self.provider_data or {}).get("thought_signature")
return sig if isinstance(sig, str) and sig else None


@dataclass
class Usage:
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/parser": "^8.59.1",
"@vitejs/plugin-react": "^6.0.1",
"concurrently": "^9.2.1",
"concurrently": "^10.0.3",
"cross-env": "^10.1.0",
"electron": "^40.9.3",
"electron-builder": "^26.8.1",
Expand All @@ -124,7 +124,8 @@
"prettier": "^3.8.3",
"rcedit": "^5.0.2",
"typescript": "^6.0.3",
"vite": "^8.0.10",
"undici": "^7.28.0",
"vite": "^8.0.16",
"vitest": "^4.1.5",
"wait-on": "^9.0.5"
},
Expand Down
Loading