dsh-plugin-subscription-oauth 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +413 -2
- package/cordis.patch.yml +13 -0
- package/lib/auth/device-flow.d.ts +112 -0
- package/lib/auth/device-flow.js +319 -0
- package/lib/auth/device-id.d.ts +31 -0
- package/lib/auth/device-id.js +58 -0
- package/lib/auth/flow.d.ts +33 -0
- package/lib/auth/flow.js +12 -0
- package/lib/auth/glm-flow.d.ts +99 -0
- package/lib/auth/glm-flow.js +194 -0
- package/lib/auth/headers.d.ts +61 -0
- package/lib/auth/headers.js +106 -0
- package/lib/auth/pkce-flow.d.ts +78 -0
- package/lib/auth/pkce-flow.js +205 -0
- package/lib/auth/provider-controller.d.ts +241 -0
- package/lib/auth/provider-controller.js +502 -0
- package/lib/auth/registry.d.ts +91 -0
- package/lib/auth/registry.js +149 -0
- package/lib/auth/rpc.d.ts +31 -0
- package/lib/auth/rpc.js +200 -0
- package/lib/auth/store.d.ts +99 -0
- package/lib/auth/store.js +213 -0
- package/lib/auth/tokens.d.ts +76 -0
- package/lib/auth/tokens.js +103 -0
- package/lib/client/SubscriptionsSection.d.ts +34 -0
- package/lib/client/SubscriptionsSection.js +742 -0
- package/lib/client/index.d.ts +36 -0
- package/lib/client/index.js +47 -0
- package/lib/client/locales.d.ts +187 -0
- package/lib/client/locales.js +185 -0
- package/lib/client/styles.d.ts +18 -0
- package/lib/client/styles.js +257 -0
- package/lib/client/subscriptions-rpc.d.ts +24 -0
- package/lib/client/subscriptions-rpc.js +44 -0
- package/lib/client.js +2078 -0
- package/lib/client.js.map +1 -0
- package/lib/constants.d.ts +79 -0
- package/lib/constants.js +91 -0
- package/lib/errors.d.ts +28 -0
- package/lib/errors.js +34 -0
- package/lib/http.d.ts +24 -0
- package/lib/http.js +98 -0
- package/lib/index.d.ts +57 -0
- package/lib/index.js +185 -0
- package/lib/protocol.d.ts +316 -0
- package/lib/protocol.js +29 -0
- package/lib/providers/antigravity-catalog.d.ts +53 -0
- package/lib/providers/antigravity-catalog.js +269 -0
- package/lib/providers/antigravity.d.ts +331 -0
- package/lib/providers/antigravity.js +821 -0
- package/lib/providers/auto.d.ts +151 -0
- package/lib/providers/auto.js +351 -0
- package/lib/providers/catalog.d.ts +69 -0
- package/lib/providers/catalog.js +78 -0
- package/lib/providers/common.d.ts +51 -0
- package/lib/providers/common.js +169 -0
- package/lib/providers/glm-provider.d.ts +41 -0
- package/lib/providers/glm-provider.js +165 -0
- package/lib/providers/glm.d.ts +159 -0
- package/lib/providers/glm.js +445 -0
- package/lib/providers/grok.d.ts +224 -0
- package/lib/providers/grok.js +700 -0
- package/lib/providers/kimi-provider.d.ts +45 -0
- package/lib/providers/kimi-provider.js +148 -0
- package/lib/providers/kimi.d.ts +151 -0
- package/lib/providers/kimi.js +400 -0
- package/lib/providers/responses-stream.d.ts +58 -0
- package/lib/providers/responses-stream.js +115 -0
- package/lib/providers/usage.d.ts +65 -0
- package/lib/providers/usage.js +309 -0
- package/lib/settings.d.ts +122 -0
- package/lib/settings.js +333 -0
- package/lib/translate/antigravity-schema.d.ts +24 -0
- package/lib/translate/antigravity-schema.js +174 -0
- package/lib/translate/antigravity.d.ts +184 -0
- package/lib/translate/antigravity.js +419 -0
- package/lib/translate/responses.d.ts +156 -0
- package/lib/translate/responses.js +395 -0
- package/lib/translate/sse.d.ts +27 -0
- package/lib/translate/sse.js +82 -0
- package/package.json +126 -4
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness messages, tools and reasoning efforts to Antigravity's Gemini-shaped
|
|
3
|
+
* `v1internal` envelope — and the response/SSE translation back.
|
|
4
|
+
*
|
|
5
|
+
* The wire is Cloud Code Assist's own dialect, not the public Gemini API: the
|
|
6
|
+
* model, project and request all ride inside one `{project, requestId, model,
|
|
7
|
+
* userAgent, requestType, request}` object, and reasoning effort becomes a
|
|
8
|
+
* `thinkingConfig` budget rather than a named level.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-subscription-oauth/translate/antigravity
|
|
11
|
+
*/
|
|
12
|
+
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
13
|
+
import type { GenerateOptions, RequestMessage, StreamChunk, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm';
|
|
14
|
+
import type { ImageResolver } from './responses.js';
|
|
15
|
+
/** One Gemini part on the v1internal wire. */
|
|
16
|
+
export interface AntigravityPart {
|
|
17
|
+
text?: string;
|
|
18
|
+
thought?: boolean;
|
|
19
|
+
thoughtSignature?: string;
|
|
20
|
+
inlineData?: {
|
|
21
|
+
mimeType: string;
|
|
22
|
+
data: string;
|
|
23
|
+
};
|
|
24
|
+
functionCall?: {
|
|
25
|
+
id?: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
args?: unknown;
|
|
28
|
+
};
|
|
29
|
+
functionResponse?: {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
response: Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** One conversation turn on the v1internal wire. */
|
|
36
|
+
export interface AntigravityContent {
|
|
37
|
+
role: 'user' | 'model';
|
|
38
|
+
parts: AntigravityPart[];
|
|
39
|
+
}
|
|
40
|
+
/** The full request envelope. */
|
|
41
|
+
export interface AntigravityRequest {
|
|
42
|
+
project: string;
|
|
43
|
+
requestId: string;
|
|
44
|
+
model: string;
|
|
45
|
+
userAgent: 'antigravity';
|
|
46
|
+
requestType: 'agent';
|
|
47
|
+
request: {
|
|
48
|
+
contents: AntigravityContent[];
|
|
49
|
+
sessionId: string;
|
|
50
|
+
systemInstruction?: {
|
|
51
|
+
parts: {
|
|
52
|
+
text: string;
|
|
53
|
+
}[];
|
|
54
|
+
};
|
|
55
|
+
tools?: {
|
|
56
|
+
functionDeclarations: Record<string, unknown>[];
|
|
57
|
+
}[];
|
|
58
|
+
toolConfig?: {
|
|
59
|
+
functionCallingConfig: {
|
|
60
|
+
mode: 'VALIDATED';
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
generationConfig?: Record<string, unknown>;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Reasoning effort ladders the Antigravity runtime families accept.
|
|
68
|
+
*
|
|
69
|
+
* An unknown family advertises none, which leaves the provider's own default in
|
|
70
|
+
* charge instead of inventing a level it may reject.
|
|
71
|
+
*
|
|
72
|
+
* @param model - model id as the live catalog reports it.
|
|
73
|
+
* @returns the reasoning block, or undefined when the family takes no level.
|
|
74
|
+
*/
|
|
75
|
+
export declare function antigravityReasoning(model: string): {
|
|
76
|
+
efforts: {
|
|
77
|
+
id: ReasoningEffortId;
|
|
78
|
+
name: string;
|
|
79
|
+
}[];
|
|
80
|
+
} | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Build the `thinkingConfig` for one request.
|
|
83
|
+
*
|
|
84
|
+
* Budgets follow the runtime family, and a budget larger than the caller's own
|
|
85
|
+
* output cap is refused locally rather than sent as a request the endpoint would
|
|
86
|
+
* reject.
|
|
87
|
+
*
|
|
88
|
+
* @param model - model id.
|
|
89
|
+
* @param effort - harness effort id, when the caller picked one.
|
|
90
|
+
* @param maxTokens - the caller's output cap, when it set one.
|
|
91
|
+
* @returns the config, or undefined when no effort was requested.
|
|
92
|
+
* @throws {LlmError} when the effort is unsupported or exceeds the output cap.
|
|
93
|
+
*/
|
|
94
|
+
export declare function antigravityThinking(model: string, effort: string | undefined, maxTokens?: number): Record<string, unknown> | undefined;
|
|
95
|
+
/** Map harness tool schemas to Gemini function declarations. */
|
|
96
|
+
export declare function toAntigravityTools(tools: readonly ToolSchema[], model?: string): {
|
|
97
|
+
functionDeclarations: Record<string, unknown>[];
|
|
98
|
+
}[];
|
|
99
|
+
/**
|
|
100
|
+
* Convert request messages into Gemini contents.
|
|
101
|
+
*
|
|
102
|
+
* Function-response names are recovered from the preceding tool calls, because
|
|
103
|
+
* the harness correlates results by id while this wire requires both id and name.
|
|
104
|
+
* Stored reasoning is not replayed: without the provider's signature the part
|
|
105
|
+
* would be rejected, so only fresh reasoning from the model travels back.
|
|
106
|
+
*
|
|
107
|
+
* @param messages - the assembled request messages.
|
|
108
|
+
* @param model - model id, which decides the signature rule.
|
|
109
|
+
* @param resolveImage - resolves an image block's bytes.
|
|
110
|
+
* @returns the contents array.
|
|
111
|
+
* @throws {LlmError} when a tool result carries no call id.
|
|
112
|
+
*/
|
|
113
|
+
export declare function toAntigravityContents(messages: readonly RequestMessage[], model?: string, resolveImage?: ImageResolver): Promise<AntigravityContent[]>;
|
|
114
|
+
/**
|
|
115
|
+
* Build one `v1internal` generate/stream request.
|
|
116
|
+
*
|
|
117
|
+
* @param options - the call's assembled options.
|
|
118
|
+
* @param projectId - the account's Cloud AI Companion project.
|
|
119
|
+
* @param system - resolved system prompt, which wins over in-band system text.
|
|
120
|
+
* @param resolveImage - resolves an image block's bytes.
|
|
121
|
+
* @param forStreaming - whether this is the streaming endpoint.
|
|
122
|
+
* @returns the envelope to POST.
|
|
123
|
+
*/
|
|
124
|
+
export declare function toAntigravityRequest(options: GenerateOptions, projectId: string, system: string | undefined, resolveImage: ImageResolver | undefined, forStreaming?: boolean): Promise<AntigravityRequest>;
|
|
125
|
+
/** The response subset this plugin reads, streamed or not. */
|
|
126
|
+
export interface AntigravityResponseEvent {
|
|
127
|
+
response?: {
|
|
128
|
+
candidates?: {
|
|
129
|
+
content?: {
|
|
130
|
+
parts?: AntigravityPart[];
|
|
131
|
+
};
|
|
132
|
+
finishReason?: string;
|
|
133
|
+
}[];
|
|
134
|
+
usageMetadata?: {
|
|
135
|
+
promptTokenCount?: number;
|
|
136
|
+
candidatesTokenCount?: number;
|
|
137
|
+
thoughtsTokenCount?: number;
|
|
138
|
+
totalTokenCount?: number;
|
|
139
|
+
cachedContentTokenCount?: number;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Map Gemini usage metadata onto the harness's disjoint counters. */
|
|
144
|
+
export declare function mapAntigravityUsage(metadata: NonNullable<NonNullable<AntigravityResponseEvent['response']>['usageMetadata']>): TokenUsage;
|
|
145
|
+
/**
|
|
146
|
+
* Push translator for Antigravity frames.
|
|
147
|
+
*
|
|
148
|
+
* Gemini streams any number of parts per frame and never announces a block, so
|
|
149
|
+
* the block boundaries are inferred: consecutive text joins one text block,
|
|
150
|
+
* consecutive thought text joins one reasoning block, and each function call is a
|
|
151
|
+
* complete block of its own.
|
|
152
|
+
*/
|
|
153
|
+
export declare class AntigravityStreamTranslator {
|
|
154
|
+
private readonly blocks;
|
|
155
|
+
private nextIndex;
|
|
156
|
+
private sawContent;
|
|
157
|
+
private sawToolCall;
|
|
158
|
+
/** Whether a terminating chunk has been produced. */
|
|
159
|
+
terminated: boolean;
|
|
160
|
+
/** Open a block and announce it. */
|
|
161
|
+
private open;
|
|
162
|
+
/** Close one block with the content it accumulated. */
|
|
163
|
+
private close;
|
|
164
|
+
/** Close every block still open, in the order they were opened. */
|
|
165
|
+
private closeAll;
|
|
166
|
+
/** The terminating chunk for one finish reason. */
|
|
167
|
+
private finish;
|
|
168
|
+
/**
|
|
169
|
+
* Process one decoded frame.
|
|
170
|
+
*
|
|
171
|
+
* @param event - one decoded `v1internal` frame.
|
|
172
|
+
* @returns the chunks it produced, in contract order.
|
|
173
|
+
*/
|
|
174
|
+
push(event: AntigravityResponseEvent): StreamChunk[];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Consume Antigravity's SSE response into the harness streaming contract.
|
|
178
|
+
*
|
|
179
|
+
* @param stream - the response body.
|
|
180
|
+
* @param onActivity - called per frame, so the caller's idle watchdog keeps up.
|
|
181
|
+
* @yields harness stream chunks in contract order.
|
|
182
|
+
* @throws {LlmError} for a malformed frame or a stream that ends without a finish.
|
|
183
|
+
*/
|
|
184
|
+
export declare function streamAntigravity(stream: ReadableStream<Uint8Array>, onActivity?: () => void): AsyncGenerator<StreamChunk>;
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness messages, tools and reasoning efforts to Antigravity's Gemini-shaped
|
|
3
|
+
* `v1internal` envelope — and the response/SSE translation back.
|
|
4
|
+
*
|
|
5
|
+
* The wire is Cloud Code Assist's own dialect, not the public Gemini API: the
|
|
6
|
+
* model, project and request all ride inside one `{project, requestId, model,
|
|
7
|
+
* userAgent, requestType, request}` object, and reasoning effort becomes a
|
|
8
|
+
* `thinkingConfig` budget rather than a named level.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-subscription-oauth/translate/antigravity
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { EMPTY_RESPONSE_CODE, LlmError, ReasoningEffortId, ToolCallId } from '@deepseek-ai/dsh-llm';
|
|
14
|
+
import { antigravityToolParameters } from './antigravity-schema.js';
|
|
15
|
+
import { parseSse } from './sse.js';
|
|
16
|
+
/**
|
|
17
|
+
* Reasoning effort ladders the Antigravity runtime families accept.
|
|
18
|
+
*
|
|
19
|
+
* An unknown family advertises none, which leaves the provider's own default in
|
|
20
|
+
* charge instead of inventing a level it may reject.
|
|
21
|
+
*
|
|
22
|
+
* @param model - model id as the live catalog reports it.
|
|
23
|
+
* @returns the reasoning block, or undefined when the family takes no level.
|
|
24
|
+
*/
|
|
25
|
+
export function antigravityReasoning(model) {
|
|
26
|
+
const ids = model.startsWith('claude-') ? ['high']
|
|
27
|
+
: model.startsWith('gpt-oss-') ? ['medium']
|
|
28
|
+
: model.startsWith('gemini-3.1-pro') || model === 'gemini-pro-agent' ? ['low', 'high']
|
|
29
|
+
: /^gemini-(?:2\.5|3)/.test(model) ? ['low', 'medium', 'high'] : [];
|
|
30
|
+
const fixed = /^gemini-.*-(low|medium|high)$/.exec(model)?.[1];
|
|
31
|
+
const efforts = fixed !== undefined && ids.includes(fixed) ? [fixed] : ids;
|
|
32
|
+
return efforts.length === 0 ? undefined : {
|
|
33
|
+
efforts: efforts.map((id) => ({ id: ReasoningEffortId(id), name: id[0]?.toUpperCase() + id.slice(1) })),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the `thinkingConfig` for one request.
|
|
38
|
+
*
|
|
39
|
+
* Budgets follow the runtime family, and a budget larger than the caller's own
|
|
40
|
+
* output cap is refused locally rather than sent as a request the endpoint would
|
|
41
|
+
* reject.
|
|
42
|
+
*
|
|
43
|
+
* @param model - model id.
|
|
44
|
+
* @param effort - harness effort id, when the caller picked one.
|
|
45
|
+
* @param maxTokens - the caller's output cap, when it set one.
|
|
46
|
+
* @returns the config, or undefined when no effort was requested.
|
|
47
|
+
* @throws {LlmError} when the effort is unsupported or exceeds the output cap.
|
|
48
|
+
*/
|
|
49
|
+
export function antigravityThinking(model, effort, maxTokens) {
|
|
50
|
+
if (effort === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const supported = antigravityReasoning(model);
|
|
53
|
+
const known = supported?.efforts.some((entry) => String(entry.id) === effort) === true;
|
|
54
|
+
if (!known) {
|
|
55
|
+
throw new LlmError(`Antigravity model ${model} does not support reasoning effort ${effort}`, 'UNSUPPORTED_REASONING_EFFORT');
|
|
56
|
+
}
|
|
57
|
+
let budget;
|
|
58
|
+
if (model.startsWith('claude-'))
|
|
59
|
+
budget = 1024;
|
|
60
|
+
else if (model.startsWith('gpt-oss-'))
|
|
61
|
+
budget = 8192;
|
|
62
|
+
else if (model.startsWith('gemini-3.1-pro') || model === 'gemini-pro-agent') {
|
|
63
|
+
budget = effort === 'high' ? 10001 : 1001;
|
|
64
|
+
}
|
|
65
|
+
else if (model.startsWith('gemini-3.5-flash') || model === 'gemini-3-flash-agent') {
|
|
66
|
+
budget = effort === 'high' ? 10000 : effort === 'medium' ? 4000 : 1000;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
budget = effort === 'high' ? -1 : effort === 'medium' ? 4000 : 1000;
|
|
70
|
+
}
|
|
71
|
+
if (maxTokens !== undefined && budget > 0 && maxTokens <= budget) {
|
|
72
|
+
throw new LlmError(`Antigravity maxTokens must exceed the thinking budget (${String(budget)}) for ${model}/${effort}`, 'INVALID_REQUEST');
|
|
73
|
+
}
|
|
74
|
+
return { includeThoughts: true, thinkingBudget: budget };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Normalize one tool result to the JSON object `FunctionResponse.response` needs.
|
|
78
|
+
*
|
|
79
|
+
* That field maps to a singular protobuf `Struct`, so a bare array, string,
|
|
80
|
+
* number or boolean is rejected upstream. Only a non-array JSON object survives
|
|
81
|
+
* verbatim; every other value is wrapped in an `output` envelope, which is the
|
|
82
|
+
* shape other Cloud Code Assist clients send.
|
|
83
|
+
*/
|
|
84
|
+
function toolResultValue(text, isError) {
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(text);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return { output: text, ...isError ? { isError: true } : {} };
|
|
91
|
+
}
|
|
92
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
93
|
+
return { ...parsed, ...isError ? { isError: true } : {} };
|
|
94
|
+
}
|
|
95
|
+
return { output: parsed, ...isError ? { isError: true } : {} };
|
|
96
|
+
}
|
|
97
|
+
/** Map harness tool schemas to Gemini function declarations. */
|
|
98
|
+
export function toAntigravityTools(tools, model = 'gemini') {
|
|
99
|
+
if (tools.length === 0)
|
|
100
|
+
return [];
|
|
101
|
+
const legacy = /^(claude-|gpt-oss-)/.test(model);
|
|
102
|
+
return [{
|
|
103
|
+
functionDeclarations: tools.map((tool) => ({
|
|
104
|
+
name: tool.name,
|
|
105
|
+
description: tool.description,
|
|
106
|
+
[legacy ? 'parameters' : 'parametersJsonSchema']: antigravityToolParameters(tool.parameters, legacy),
|
|
107
|
+
})),
|
|
108
|
+
}];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Placeholder a Gemini 3 model needs in place of a signature this plugin did not
|
|
112
|
+
* record.
|
|
113
|
+
*
|
|
114
|
+
* Gemini 3 validates the thought signature on the first function call of every
|
|
115
|
+
* model tool-call step. This plugin does not persist signatures across turns, so
|
|
116
|
+
* it sends the documented validator-skip marker instead of failing the turn.
|
|
117
|
+
*/
|
|
118
|
+
const SKIP_THOUGHT_SIGNATURE_VALIDATOR = 'skip_thought_signature_validator';
|
|
119
|
+
/** Whether a model validates thought signatures on the first function call. */
|
|
120
|
+
function requiresThoughtSignatures(model) {
|
|
121
|
+
return model !== undefined && /^gemini-3(?:[.-]|$)/i.test(model);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Convert request messages into Gemini contents.
|
|
125
|
+
*
|
|
126
|
+
* Function-response names are recovered from the preceding tool calls, because
|
|
127
|
+
* the harness correlates results by id while this wire requires both id and name.
|
|
128
|
+
* Stored reasoning is not replayed: without the provider's signature the part
|
|
129
|
+
* would be rejected, so only fresh reasoning from the model travels back.
|
|
130
|
+
*
|
|
131
|
+
* @param messages - the assembled request messages.
|
|
132
|
+
* @param model - model id, which decides the signature rule.
|
|
133
|
+
* @param resolveImage - resolves an image block's bytes.
|
|
134
|
+
* @returns the contents array.
|
|
135
|
+
* @throws {LlmError} when a tool result carries no call id.
|
|
136
|
+
*/
|
|
137
|
+
export async function toAntigravityContents(messages, model, resolveImage) {
|
|
138
|
+
const out = [];
|
|
139
|
+
const callNames = new Map();
|
|
140
|
+
for (const message of messages) {
|
|
141
|
+
if (message.role === 'system' || message.role === 'developer')
|
|
142
|
+
continue;
|
|
143
|
+
if (message.role === 'tool') {
|
|
144
|
+
const id = String(message.toolCallId);
|
|
145
|
+
if (id.length === 0)
|
|
146
|
+
throw new LlmError('tool result has no call id', 'INVALID_REQUEST');
|
|
147
|
+
const text = message.content.map((block) => block.type === 'text' ? block.text : '').join('');
|
|
148
|
+
const part = {
|
|
149
|
+
functionResponse: {
|
|
150
|
+
id,
|
|
151
|
+
name: callNames.get(id) ?? '',
|
|
152
|
+
response: toolResultValue(text, message.isError === true),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
const previous = out.at(-1);
|
|
156
|
+
if (previous?.role === 'user')
|
|
157
|
+
previous.parts.push(part);
|
|
158
|
+
else
|
|
159
|
+
out.push({ role: 'user', parts: [part] });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const role = message.role === 'assistant' ? 'model' : 'user';
|
|
163
|
+
const parts = [];
|
|
164
|
+
let sawFunctionCall = false;
|
|
165
|
+
for (const block of message.content) {
|
|
166
|
+
switch (block.type) {
|
|
167
|
+
case 'text':
|
|
168
|
+
// Antigravity's Claude-backed models reject an empty text part.
|
|
169
|
+
parts.push({ text: block.text.trim().length > 0 ? block.text : '.' });
|
|
170
|
+
break;
|
|
171
|
+
case 'image': {
|
|
172
|
+
const resolved = resolveImage === undefined ? undefined : await resolveImage(block.attachment);
|
|
173
|
+
if (resolved !== undefined) {
|
|
174
|
+
parts.push({ inlineData: { mimeType: resolved.mediaType, data: resolved.base64 } });
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case 'tool-call': {
|
|
179
|
+
callNames.set(String(block.id), block.name);
|
|
180
|
+
let args;
|
|
181
|
+
try {
|
|
182
|
+
args = JSON.parse(block.arguments);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
args = {};
|
|
186
|
+
}
|
|
187
|
+
const thoughtSignature = !sawFunctionCall && requiresThoughtSignatures(model)
|
|
188
|
+
? SKIP_THOUGHT_SIGNATURE_VALIDATOR
|
|
189
|
+
: undefined;
|
|
190
|
+
parts.push({
|
|
191
|
+
functionCall: { id: String(block.id), name: block.name, args },
|
|
192
|
+
...thoughtSignature === undefined ? {} : { thoughtSignature },
|
|
193
|
+
});
|
|
194
|
+
sawFunctionCall = true;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
// Reasoning is not replayed without its provider signature, and file
|
|
199
|
+
// blocks never reach a provider natively.
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (parts.length === 0)
|
|
204
|
+
continue;
|
|
205
|
+
const previous = out.at(-1);
|
|
206
|
+
if (previous?.role === role)
|
|
207
|
+
previous.parts.push(...parts);
|
|
208
|
+
else
|
|
209
|
+
out.push({ role, parts });
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Build one `v1internal` generate/stream request.
|
|
215
|
+
*
|
|
216
|
+
* @param options - the call's assembled options.
|
|
217
|
+
* @param projectId - the account's Cloud AI Companion project.
|
|
218
|
+
* @param system - resolved system prompt, which wins over in-band system text.
|
|
219
|
+
* @param resolveImage - resolves an image block's bytes.
|
|
220
|
+
* @param forStreaming - whether this is the streaming endpoint.
|
|
221
|
+
* @returns the envelope to POST.
|
|
222
|
+
*/
|
|
223
|
+
export async function toAntigravityRequest(options, projectId, system, resolveImage, forStreaming = false) {
|
|
224
|
+
const tools = toAntigravityTools(options.tools ?? [], options.model);
|
|
225
|
+
const thinkingConfig = antigravityThinking(options.model, options.reasoningEffort, options.maxTokens);
|
|
226
|
+
let maxOutputTokens = options.maxTokens;
|
|
227
|
+
// Claude models on the streaming path reject an output cap of 65535 or more.
|
|
228
|
+
if (forStreaming && maxOutputTokens !== undefined && /^claude-/.test(options.model) && maxOutputTokens >= 65535) {
|
|
229
|
+
maxOutputTokens = 64000;
|
|
230
|
+
}
|
|
231
|
+
const generationConfig = {
|
|
232
|
+
...maxOutputTokens === undefined ? {} : { maxOutputTokens },
|
|
233
|
+
...options.temperature === undefined ? {} : { temperature: options.temperature },
|
|
234
|
+
...options.stop === undefined || options.stop.length === 0 ? {} : { stopSequences: options.stop },
|
|
235
|
+
...thinkingConfig === undefined ? {} : { thinkingConfig },
|
|
236
|
+
};
|
|
237
|
+
const sessionId = options.sessionId === undefined ? randomUUID() : String(options.sessionId);
|
|
238
|
+
return {
|
|
239
|
+
project: projectId,
|
|
240
|
+
requestId: `agent/${String(Date.now())}/${randomUUID()}/4`,
|
|
241
|
+
model: options.model,
|
|
242
|
+
userAgent: 'antigravity',
|
|
243
|
+
requestType: 'agent',
|
|
244
|
+
request: {
|
|
245
|
+
contents: await toAntigravityContents(options.messages, options.model, resolveImage),
|
|
246
|
+
sessionId,
|
|
247
|
+
...system === undefined || system.length === 0 ? {} : { systemInstruction: { parts: [{ text: system }] } },
|
|
248
|
+
...tools.length === 0 ? {} : {
|
|
249
|
+
tools,
|
|
250
|
+
toolConfig: { functionCallingConfig: { mode: 'VALIDATED' } },
|
|
251
|
+
},
|
|
252
|
+
...Object.keys(generationConfig).length === 0 ? {} : { generationConfig },
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** Map Gemini usage metadata onto the harness's disjoint counters. */
|
|
257
|
+
export function mapAntigravityUsage(metadata) {
|
|
258
|
+
const cached = metadata.cachedContentTokenCount ?? 0;
|
|
259
|
+
return {
|
|
260
|
+
// Gemini's prompt count already includes the cached prefix; the harness
|
|
261
|
+
// reports cache reads separately, so it must not be double counted.
|
|
262
|
+
inputTokens: Math.max(0, (metadata.promptTokenCount ?? 0) - cached),
|
|
263
|
+
outputTokens: metadata.candidatesTokenCount ?? 0,
|
|
264
|
+
...cached > 0 ? { cacheReadTokens: cached } : {},
|
|
265
|
+
...metadata.thoughtsTokenCount === undefined ? {} : { reasoningTokens: metadata.thoughtsTokenCount },
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Push translator for Antigravity frames.
|
|
270
|
+
*
|
|
271
|
+
* Gemini streams any number of parts per frame and never announces a block, so
|
|
272
|
+
* the block boundaries are inferred: consecutive text joins one text block,
|
|
273
|
+
* consecutive thought text joins one reasoning block, and each function call is a
|
|
274
|
+
* complete block of its own.
|
|
275
|
+
*/
|
|
276
|
+
export class AntigravityStreamTranslator {
|
|
277
|
+
blocks = new Map();
|
|
278
|
+
nextIndex = 0;
|
|
279
|
+
sawContent = false;
|
|
280
|
+
sawToolCall = false;
|
|
281
|
+
/** Whether a terminating chunk has been produced. */
|
|
282
|
+
terminated = false;
|
|
283
|
+
/** Open a block and announce it. */
|
|
284
|
+
open(key, kind, chunks, values = {}) {
|
|
285
|
+
const block = { index: this.nextIndex++, kind, text: '', ...values };
|
|
286
|
+
this.blocks.set(key, block);
|
|
287
|
+
chunks.push({ type: 'block-start', index: block.index, blockType: kind });
|
|
288
|
+
return block;
|
|
289
|
+
}
|
|
290
|
+
/** Close one block with the content it accumulated. */
|
|
291
|
+
close(key, chunks) {
|
|
292
|
+
const block = this.blocks.get(key);
|
|
293
|
+
if (block === undefined)
|
|
294
|
+
return;
|
|
295
|
+
this.blocks.delete(key);
|
|
296
|
+
let content;
|
|
297
|
+
if (block.kind === 'text')
|
|
298
|
+
content = { type: 'text', text: block.text };
|
|
299
|
+
else if (block.kind === 'reasoning')
|
|
300
|
+
content = { type: 'reasoning', text: block.text };
|
|
301
|
+
else
|
|
302
|
+
content = {
|
|
303
|
+
type: 'tool-call',
|
|
304
|
+
id: ToolCallId(block.id ?? `call_${randomUUID().replaceAll('-', '')}`),
|
|
305
|
+
name: block.name ?? '',
|
|
306
|
+
arguments: block.text,
|
|
307
|
+
};
|
|
308
|
+
chunks.push({ type: 'block-end', index: block.index, block: content });
|
|
309
|
+
}
|
|
310
|
+
/** Close every block still open, in the order they were opened. */
|
|
311
|
+
closeAll(chunks) {
|
|
312
|
+
for (const key of [...this.blocks.keys()])
|
|
313
|
+
this.close(key, chunks);
|
|
314
|
+
}
|
|
315
|
+
/** The terminating chunk for one finish reason. */
|
|
316
|
+
finish(reason) {
|
|
317
|
+
if (!this.sawContent) {
|
|
318
|
+
return {
|
|
319
|
+
type: 'finish',
|
|
320
|
+
reason: { kind: 'error', failure: { message: 'Antigravity returned no content', code: EMPTY_RESPONSE_CODE } },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (reason === 'MAX_TOKENS')
|
|
324
|
+
return { type: 'finish', reason: { kind: 'max-tokens' } };
|
|
325
|
+
if (reason === 'SAFETY' || reason === 'RECITATION' || reason === 'BLOCKLIST') {
|
|
326
|
+
return {
|
|
327
|
+
type: 'finish',
|
|
328
|
+
reason: {
|
|
329
|
+
kind: 'error',
|
|
330
|
+
failure: { message: `Antigravity blocked the response (${reason})`, code: 'CONTENT_FILTER' },
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return { type: 'finish', reason: { kind: this.sawToolCall ? 'tool-calls' : 'stop' } };
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Process one decoded frame.
|
|
338
|
+
*
|
|
339
|
+
* @param event - one decoded `v1internal` frame.
|
|
340
|
+
* @returns the chunks it produced, in contract order.
|
|
341
|
+
*/
|
|
342
|
+
push(event) {
|
|
343
|
+
if (this.terminated)
|
|
344
|
+
return [];
|
|
345
|
+
const chunks = [];
|
|
346
|
+
const candidate = event.response?.candidates?.[0];
|
|
347
|
+
for (const [partIndex, part] of (candidate?.content?.parts ?? []).entries()) {
|
|
348
|
+
if (part.thought === true && typeof part.text === 'string' && part.text.length > 0) {
|
|
349
|
+
const block = this.blocks.get('reasoning') ?? this.open('reasoning', 'reasoning', chunks);
|
|
350
|
+
block.text += part.text;
|
|
351
|
+
this.sawContent = true;
|
|
352
|
+
chunks.push({ type: 'reasoning-delta', index: block.index, text: part.text });
|
|
353
|
+
}
|
|
354
|
+
else if (typeof part.text === 'string' && part.text.length > 0) {
|
|
355
|
+
const block = this.blocks.get('text') ?? this.open('text', 'text', chunks);
|
|
356
|
+
block.text += part.text;
|
|
357
|
+
this.sawContent = true;
|
|
358
|
+
chunks.push({ type: 'text-delta', index: block.index, text: part.text });
|
|
359
|
+
}
|
|
360
|
+
else if (part.functionCall !== undefined) {
|
|
361
|
+
const call = part.functionCall;
|
|
362
|
+
const id = typeof call.id === 'string' && call.id.length > 0
|
|
363
|
+
? call.id
|
|
364
|
+
: `call_${randomUUID().replaceAll('-', '')}`;
|
|
365
|
+
const key = `call:${id}:${String(partIndex)}`;
|
|
366
|
+
const args = JSON.stringify(call.args ?? {});
|
|
367
|
+
const block = this.open(key, 'tool-call', chunks, { id, name: call.name ?? '' });
|
|
368
|
+
block.text = args;
|
|
369
|
+
this.sawContent = true;
|
|
370
|
+
this.sawToolCall = true;
|
|
371
|
+
chunks.push({
|
|
372
|
+
type: 'tool-call-delta',
|
|
373
|
+
index: block.index,
|
|
374
|
+
id: ToolCallId(id),
|
|
375
|
+
name: call.name ?? '',
|
|
376
|
+
argumentsDelta: args,
|
|
377
|
+
});
|
|
378
|
+
this.close(key, chunks);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (candidate?.finishReason !== undefined) {
|
|
382
|
+
this.closeAll(chunks);
|
|
383
|
+
const usage = event.response?.usageMetadata;
|
|
384
|
+
if (usage !== undefined)
|
|
385
|
+
chunks.push({ type: 'usage', usage: mapAntigravityUsage(usage) });
|
|
386
|
+
chunks.push(this.finish(candidate.finishReason));
|
|
387
|
+
this.terminated = true;
|
|
388
|
+
}
|
|
389
|
+
return chunks;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Consume Antigravity's SSE response into the harness streaming contract.
|
|
394
|
+
*
|
|
395
|
+
* @param stream - the response body.
|
|
396
|
+
* @param onActivity - called per frame, so the caller's idle watchdog keeps up.
|
|
397
|
+
* @yields harness stream chunks in contract order.
|
|
398
|
+
* @throws {LlmError} for a malformed frame or a stream that ends without a finish.
|
|
399
|
+
*/
|
|
400
|
+
export async function* streamAntigravity(stream, onActivity) {
|
|
401
|
+
const translator = new AntigravityStreamTranslator();
|
|
402
|
+
for await (const event of parseSse(stream, onActivity)) {
|
|
403
|
+
if (event.data === '[DONE]')
|
|
404
|
+
break;
|
|
405
|
+
let parsed;
|
|
406
|
+
try {
|
|
407
|
+
parsed = JSON.parse(event.data);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
throw new LlmError(`malformed Antigravity SSE payload: ${event.data.slice(0, 120)}`, 'MALFORMED_RESPONSE');
|
|
411
|
+
}
|
|
412
|
+
yield* translator.push(parsed);
|
|
413
|
+
if (translator.terminated)
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (!translator.terminated) {
|
|
417
|
+
throw new LlmError('Antigravity SSE stream ended before a finish chunk', 'STREAM_CLOSED');
|
|
418
|
+
}
|
|
419
|
+
}
|