Last Updated: December 2025
This document explains how the Antigravity plugin works, including the request/response flow, Claude-specific handling, and session recovery.
┌─────────────────────────────────────────────────────────────────┐
│ OpenCode ──▶ Plugin ──▶ Antigravity API ──▶ Claude/Gemini │
│ │ │ │ │ │
│ │ │ │ └─ Model │
│ │ │ └─ Google's gateway (Gemini fmt) │
│ │ └─ THIS PLUGIN (auth, transform, recovery) │
│ └─ AI coding assistant │
└─────────────────────────────────────────────────────────────────┘
The plugin intercepts requests to generativelanguage.googleapis.com, transforms them for the Antigravity API, and handles authentication, rate limits, and error recovery.
src/
├── index.ts # Plugin exports
├── plugin.ts # Main entry, fetch interceptor
├── constants.ts # Endpoints, headers, config
├── antigravity/
│ └── oauth.ts # OAuth token exchange
└── plugin/
├── auth.ts # Token validation & refresh
├── request.ts # Request transformation (main logic)
├── request-helpers.ts # Schema cleaning, thinking filters
├── thinking-recovery.ts # Turn boundary detection, crash recovery
├── recovery.ts # Session recovery (tool_result_missing)
├── quota.ts # Quota checking (API usage stats)
├── cache.ts # Auth & signature caching
├── cache/
│ └── signature-cache.ts # Disk-based signature persistence
├── config/
│ ├── schema.ts # Zod config schema
│ └── loader.ts # Config file loading
├── accounts.ts # Multi-account management
├── server.ts # OAuth callback server
└── debug.ts # Debug logging
fetch() intercepted → isGenerativeLanguageRequest() → prepareAntigravityRequest()- Account selection (round-robin, rate-limit aware)
- Token refresh if expired
- Endpoint fallback (daily → autopush → prod)
| Step | What Happens |
|---|---|
| Model detection | Detect Claude/Gemini from URL |
| Thinking config | Add thinkingConfig for thinking models |
| Thinking strip | Remove ALL thinking blocks (Claude) |
| Tool normalization | Convert to functionDeclarations[] |
| Schema cleaning | Remove unsupported JSON Schema fields |
| ID assignment | Assign IDs to tool calls (FIFO matching) |
| Wrap request | { project, model, request: {...} } |
| Step | What Happens |
|---|---|
| SSE streaming | Real-time line-by-line TransformStream |
| Signature caching | Cache thoughtSignature for display |
| Format transform | thought: true → type: "reasoning" |
| Envelope unwrap | Extract inner response object |
Claude through Antigravity requires:
- Gemini format -
contents[].parts[]notmessages[].content[] - Thinking signatures - Multi-turn needs signed blocks or errors
- Schema restrictions - Rejects
const,$ref,$defs, etc. - Tool validation -
VALIDATEDmode requires proper schemas
Problem: OpenCode stores thinking blocks, but may corrupt signatures.
Solution: Strip ALL thinking blocks from outgoing requests.
Turn 1 Response: { thought: true, text: "...", thoughtSignature: "abc" }
↓ (stored by OpenCode, possibly corrupted)
Turn 2 Request: Plugin STRIPS all thinking blocks
↓
Claude API: Generates fresh thinking
Why this works:
- Zero signature errors (impossible to have invalid signatures)
- Same quality (Claude sees full conversation, re-thinks fresh)
- Simpler code (no complex validation/restoration)
Claude API requires thinking before tool_use blocks. The plugin:
- Caches signed thinking from responses (
lastSignedThinkingBySessionKey) - On subsequent requests, injects cached thinking before tool_use
- Only injects for the first assistant message of a turn (not every message)
Turn boundary detection (thinking-recovery.ts):
// A "turn" starts after a real user message (not tool_result)
// Only inject thinking into first assistant message after thatWhen a tool execution is interrupted (ESC, timeout, crash):
Error: tool_use ids were found without tool_result blocks immediately after
Recovery flow (recovery.ts):
- Detect error via
session.errorevent - Fetch session messages via
client.session.messages() - Extract
tool_useIDs from failed message - Inject synthetic
tool_resultblocks:{ type: "tool_result", tool_use_id: id, content: "Operation cancelled" }
- Send via
client.session.prompt() - Optionally auto-resume with "continue"
Error: Expected thinking but found text
Recovery (thinking-recovery.ts):
- Detect conversation is in tool loop without thinking at turn start
- Close the corrupted turn with synthetic messages
- Start fresh turn where Claude can generate new thinking
Claude rejects unsupported JSON Schema features. The plugin uses an allowlist approach:
Kept: type, properties, required, description, enum, items
Removed: const, $ref, $defs, default, examples, additionalProperties, $schema, title
Transformations:
const: "value"→enum: ["value"]- Empty object schema → Add placeholder
reasonproperty
- Sticky selection - Same account until rate limited (preserves cache)
- Per-model-family - Claude/Gemini rate limits tracked separately
- Dual quota (Gemini) - Antigravity + Gemini CLI headers
- Automatic failover - On 429, switch to next available account
Location: ~/.config/opencode/antigravity-accounts.json
Contains OAuth refresh tokens - treat as sensitive.
| Variable | Purpose |
|---|---|
OPENCODE_ANTIGRAVITY_DEBUG |
1 or 2 for file debug logging |
OPENCODE_ANTIGRAVITY_DEBUG_TUI |
1 or true for TUI log panel debug output |
OPENCODE_ANTIGRAVITY_QUIET |
Suppress toast notifications |
debug and debug_tui are independent sinks: debug controls file logs, while debug_tui controls TUI logs.
Location: ~/.config/opencode/antigravity.json
{
"session_recovery": true,
"auto_resume": true,
"resume_text": "continue",
"keep_thinking": false
}| Function | Purpose |
|---|---|
prepareAntigravityRequest() |
Main request transformation |
transformAntigravityResponse() |
SSE streaming, format conversion |
ensureThinkingBeforeToolUseInContents() |
Inject cached thinking |
createStreamingTransformer() |
Real-time SSE processing |
| Function | Purpose |
|---|---|
deepFilterThinkingBlocks() |
Recursive thinking block removal |
cleanJSONSchemaForAntigravity() |
Schema sanitization |
transformThinkingParts() |
thought → reasoning format |
| Function | Purpose |
|---|---|
analyzeConversationState() |
Detect turn boundaries, tool loops |
needsThinkingRecovery() |
Check if recovery needed |
closeToolLoopForThinking() |
Inject synthetic messages |
| Function | Purpose |
|---|---|
handleSessionRecovery() |
Main recovery orchestration |
createSessionRecoveryHook() |
Hook factory for plugin |
export OPENCODE_ANTIGRAVITY_DEBUG=2 # Verbose file logs
export OPENCODE_ANTIGRAVITY_DEBUG_TUI=1 # TUI log panel output~/.config/opencode/antigravity-logs/
- Is
isClaudeModeltrue for Claude models? - Are thinking blocks being stripped?
- Are tool schemas being cleaned?
- Is session recovery triggering?
| Error | Cause | Solution |
|---|---|---|
invalid signature |
Corrupted thinking block | Update plugin (strips all thinking) |
Unknown field: const |
Schema uses const |
Plugin auto-converts to enum |
tool_use without tool_result |
Interrupted execution | Session recovery injects results |
Expected thinking but found text |
Turn started without thinking | Thinking recovery closes turn |
429 Too Many Requests |
Rate limited | Plugin auto-rotates accounts |
- ANTIGRAVITY_API_SPEC.md - API reference
- README.md - Installation & usage