Skip to content

Commit 09509c3

Browse files
author
tctinh
committed
Merge branch 'main' of github.com:tctinh/opencode-antigravity-auth
2 parents a85e6be + 85d2847 commit 09509c3

2 files changed

Lines changed: 103 additions & 20 deletions

File tree

src/plugin/request-helpers.ts

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -153,23 +153,9 @@ export function filterUnsignedThinkingBlocks(contents: any[]): any[] {
153153
}
154154

155155
/**
156-
* Transforms Anthropic-style thinking blocks (type: "thinking") to reasoning format.
157-
*/
158-
function transformAnthropicThinkingBlocks(content: any[]): any[] {
159-
return content.map((block: any) => {
160-
if (block && typeof block === "object" && block.type === "thinking") {
161-
return {
162-
type: "reasoning",
163-
text: block.text || "",
164-
...(block.signature ? { signature: block.signature } : {}),
165-
};
166-
}
167-
return block;
168-
});
169-
}
170-
171-
/**
172-
* Transforms Gemini-style thought parts (thought: true) to reasoning format.
156+
* Transforms Gemini-style thought parts (thought: true) and Anthropic-style
157+
* thinking parts (type: "thinking") to reasoning format.
158+
* Claude responses through Antigravity may use candidates structure with Anthropic-style parts.
173159
*/
174160
function transformGeminiCandidate(candidate: any): any {
175161
if (!candidate || typeof candidate !== "object") {
@@ -183,10 +169,28 @@ function transformGeminiCandidate(candidate: any): any {
183169

184170
const thinkingTexts: string[] = [];
185171
const transformedParts = content.parts.map((part: any) => {
186-
if (part && typeof part === "object" && part.thought === true) {
172+
if (!part || typeof part !== "object") {
173+
return part;
174+
}
175+
176+
// Handle Gemini-style: thought: true
177+
if (part.thought === true) {
187178
thinkingTexts.push(part.text || "");
188179
return { ...part, type: "reasoning" };
189180
}
181+
182+
// Handle Anthropic-style in candidates: type: "thinking"
183+
if (part.type === "thinking") {
184+
const thinkingText = part.thinking || part.text || "";
185+
thinkingTexts.push(thinkingText);
186+
return {
187+
...part,
188+
type: "reasoning",
189+
text: thinkingText,
190+
thought: true,
191+
};
192+
}
193+
190194
return part;
191195
});
192196

@@ -200,6 +204,7 @@ function transformGeminiCandidate(candidate: any): any {
200204
/**
201205
* Transforms thinking/reasoning content in response parts to OpenCode's expected format.
202206
* Handles both Gemini-style (thought: true) and Anthropic-style (type: "thinking") formats.
207+
* Also extracts reasoning_content for Anthropic-style responses.
203208
*/
204209
export function transformThinkingParts(response: unknown): unknown {
205210
if (!response || typeof response !== "object") {
@@ -208,15 +213,38 @@ export function transformThinkingParts(response: unknown): unknown {
208213

209214
const resp = response as Record<string, unknown>;
210215
const result: Record<string, unknown> = { ...resp };
216+
const reasoningTexts: string[] = [];
211217

218+
// Handle Anthropic-style content array (type: "thinking")
212219
if (Array.isArray(resp.content)) {
213-
result.content = transformAnthropicThinkingBlocks(resp.content);
220+
const transformedContent: any[] = [];
221+
for (const block of resp.content) {
222+
if (block && typeof block === "object" && (block as any).type === "thinking") {
223+
const thinkingText = (block as any).thinking || (block as any).text || "";
224+
reasoningTexts.push(thinkingText);
225+
transformedContent.push({
226+
...block,
227+
type: "reasoning",
228+
text: thinkingText,
229+
thought: true,
230+
});
231+
} else {
232+
transformedContent.push(block);
233+
}
234+
}
235+
result.content = transformedContent;
214236
}
215237

238+
// Handle Gemini-style candidates array
216239
if (Array.isArray(resp.candidates)) {
217240
result.candidates = resp.candidates.map(transformGeminiCandidate);
218241
}
219242

243+
// Add reasoning_content if we found any thinking blocks (for Anthropic-style)
244+
if (reasoningTexts.length > 0 && !result.reasoning_content) {
245+
result.reasoning_content = reasoningTexts.join("\n\n");
246+
}
247+
220248
return result;
221249
}
222250

src/plugin/request.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,60 @@ function transformStreamingPayload(payload: string): string {
6363
.join("\n");
6464
}
6565

66+
/**
67+
* Creates a TransformStream that processes SSE chunks incrementally,
68+
* transforming each line as it arrives for true streaming support.
69+
*/
70+
function createStreamingTransformer(): TransformStream<Uint8Array, Uint8Array> {
71+
const decoder = new TextDecoder();
72+
const encoder = new TextEncoder();
73+
let buffer = "";
74+
75+
return new TransformStream({
76+
transform(chunk, controller) {
77+
buffer += decoder.decode(chunk, { stream: true });
78+
79+
// Process complete lines
80+
const lines = buffer.split("\n");
81+
// Keep the last incomplete line in buffer
82+
buffer = lines.pop() || "";
83+
84+
for (const line of lines) {
85+
const transformedLine = transformSseLine(line);
86+
controller.enqueue(encoder.encode(transformedLine + "\n"));
87+
}
88+
},
89+
flush(controller) {
90+
// Process any remaining data in buffer
91+
if (buffer) {
92+
const transformedLine = transformSseLine(buffer);
93+
controller.enqueue(encoder.encode(transformedLine));
94+
}
95+
},
96+
});
97+
}
98+
99+
/**
100+
* Transforms a single SSE line, extracting and transforming the inner response.
101+
*/
102+
function transformSseLine(line: string): string {
103+
if (!line.startsWith("data:")) {
104+
return line;
105+
}
106+
const json = line.slice(5).trim();
107+
if (!json) {
108+
return line;
109+
}
110+
try {
111+
const parsed = JSON.parse(json) as { response?: unknown };
112+
if (parsed.response !== undefined) {
113+
const transformed = transformThinkingParts(parsed.response);
114+
return `data: ${JSON.stringify(transformed)}`;
115+
}
116+
} catch (_) { }
117+
return line;
118+
}
119+
66120
/**
67121
* Rewrites OpenAI-style requests into Antigravity shape, normalizing model, headers,
68122
* optional cached_content, and thinking config. Also toggles streaming mode for SSE actions.
@@ -509,6 +563,8 @@ export function prepareAntigravityRequest(
509563
/**
510564
* Normalizes Antigravity responses: applies retry headers, extracts cache usage into headers,
511565
* rewrites preview errors, flattens streaming payloads, and logs debug metadata.
566+
*
567+
* For streaming SSE responses, uses TransformStream for true incremental streaming.
512568
*/
513569
export async function transformAntigravityResponse(
514570
response: Response,
@@ -587,7 +643,6 @@ export async function transformAntigravityResponse(
587643

588644
try {
589645
const text = await response.text();
590-
const headers = new Headers(response.headers);
591646

592647
if (!response.ok) {
593648
let errorBody;

0 commit comments

Comments
 (0)