mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 20:52:15 +03:00
* fix(antigravity): heal empty-projectId accounts via retryable auto-onboarding Accounts with an empty Cloud Code projectId get a permanent 422 "Missing Google projectId" when loadCodeAssist returns no project. The 3.8.50 bootstrap attempts to CREATE the project via onboardUser, but a single failed attempt (transient network/upstream error) was memoized forever in onboardAttemptedCache: every later request in the process skipped onboarding and 422'd, even though a retry would succeed. Replace the permanent per-token Set with a failure-backoff map: failed onboard attempts are retried after a 5-minute backoff (bounded, self-healing), the in-flight lock still dedupes concurrent calls, and success clears the failure marker and memoizes the project as before. Accounts that CAN be onboarded now heal automatically on a later request or token refresh — no user action. Tests: the existing "does not retry" case is now framed as the backoff window; a new case proves the account heals (retries onboarding and recovers the project) once the backoff expires. * chore(changelog): fragment for #10424 antigravity project autocreate * feat(antigravity): BYOP fast-fail + manual GCP project-id override Port decolua/9router#2934 + VansRouter 802a859: - tryOnboardUser now returns a three-way status; a 200 onboardUser response WITHOUT cloudaicompanionProject means Google deprecated automatic project creation for standard-tier (personal) accounts (BYOP). Such accounts are cached permanently (no pointless ~18s re-onboard) and the executor fails fast with 403 GCP_PROJECT_REQUIRED + actionable 'enter your project id' message instead of the generic 422 or a delayed 429. - Transient onboard failures keep the existing 5-min backoff heal. - Manual project-id override: the EditConnectionModal now stamps providerSpecificData.isProjectIdManual when the operator enters a project id, and tokenRefresh skips auto-discovery for flagged accounts so the manual value is never overwritten. * chore(changelog): cover BYOP fast-fail + manual override in #10424 fragment * test(antigravity): expect fast 403 GCP_PROJECT_REQUIRED when loadCodeAssist finds no project (#10424) Google now marks accounts without an onboarded project as BYOP (automatic project creation deprecated for standard-tier accounts, #2934). The PR's BYOP fast-fail path returns 403 gcp_project_required instead of the old generic 422 missing_project_id; align the #2334 executor test with that contract so CI unit-test shard 2/4 passes. * fix(antigravity): persist isProjectIdManual, fix BYOP citation, dodge refresh-retry Review follow-up on #10424: 1. EditConnectionModal: isProjectIdManual was set on updates.providerSpecificData right after the project-id field, then the OAuth path (Antigravity is always OAuth) rebuilt providerSpecificData from connection.providerSpecificData before the request went out, discarding the flag — tokenRefresh.ts was guarding a field never actually persisted. The flag now lands in the single surviving antigravity merge, with a jsdom regression test (modeled on edit-connection-modal-openai-store-toggle). 2. The '#2934' citation for the Google BYOP claim pointed at an unrelated closed issue. Swapped for the real tracking issue #8491 (empty Google projectId -> 422 class) across bootstrap/executor/test comments. 3. BYOP fast-fail now returns 422 instead of 403: chatCore's generic 401/403 -> refresh-and-retry path was hitting Google's OAuth token endpoint on every request from an affected account (pointless — refreshing cannot create a GCP project), and 422 matches the sibling missing_project_id error the client already maps to an action-needed prompt. Also: eslint-disable-next-line for the pre-existing react-hooks/set-state-in-effect baseline noise in the modal (repo convention, same pattern as 11 other dashboard files). * chore(ci): drop unused eslint-disable in EditConnectionModal form hydration The react-hooks/set-state-in-effect disable added in the previous commit is unused under the repo's pinned eslint-plugin-react-hooks (7.0.1) — the rule does not fire on this line at that version, so the unused directive tripped the whole-repo 'No new ESLint warnings' gate (max-warnings 0). Verified with the lockfile-pinned plugin: lint:json is clean (0 errors, 0 warnings). * fix(build): bound and retry the opencode-plugin npm install in prepublish The plugin's node_modules is gitignored, so every fresh CI checkout runs a full npm install inside @omniroute/opencode-plugin during build:cli. npm's unbounded fetch retries turn a stalled registry CDN connection (the recurring onnxruntime-class ETIMEDOUT flake) into a 20-30 minute hang — the DAST 'Build CLI bundle' step has been cancelled at the 30m cap repeatedly. - Bound npm fetch: --fetch-timeout 60s, 2 retries with capped backoff — a stalled connection now fails fast instead of hanging the job. - Retry the install up to 3 times with a 10s pause between attempts, so transient CDN failures recover in-build. Net effect: the step either completes (network OK) or fails quickly with a clear error (network down) — it can no longer eat the whole job budget. * ci(quality): use the npm-ci-retry action on every install step Fast Quality Gates failed on the recurring onnxruntime-node postinstall ETIMEDOUT (Microsoft CDN 150.171.x.x) - the same transient flake that has hit Vitest and dast-smoke today. Only the Build job used the retry action; the other five jobs (Docs, Fast Quality Gates, Vitest, Unit Tests, changelog) still ran a bare install and die on any CDN hiccup. Use the existing retry action (3 attempts, exponential backoff) on every install step for consistency. * Merge branch 'release/v3.8.50' into fix/antigravity-project-autocreate * test(fix): refresh expired alibaba quota sample validity and onnxruntime pin for v3.8.50 base - alibaba-free-tier-quota-fetcher.test.ts: sample quotaValidityPeriod (2026-08-16 16:00 UTC) is in the past, making every quota entry classify as expired/not_capable; bump to 2028-01-01 UTC so the text/merge classification tests exercise the intended path again. - optional-transformers-dependency.test.ts: onnxruntime-node pin assertion updated from ~1.24.3 to ~1.27.0 to match package.json (bumped by #10403); the regular-not-optional intent is unchanged. * test(fix): widen modelsDevSync lastSync wait from 200ms default to 2000ms The truthy-spellings loop asserted each enabled case completes its first fetch within waitFor's 200ms default timeout, which trips under CI runner load (observed on PR 10424 shard 2/4). Match the file's other lastSync waits (2000ms) so the sync-completion assertion is load-tolerant. --------- Co-authored-by: Rouzbeh <rqzbeh@users.noreply.github.com>
1103 lines
36 KiB
TypeScript
1103 lines
36 KiB
TypeScript
import { test } from "node:test";
|
||
import assert from "node:assert/strict";
|
||
|
||
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
|
||
import { setCliCompatProviders } from "../../open-sse/config/cliFingerprints.ts";
|
||
import { scrubProxyAndFingerprintHeaders } from "../../open-sse/services/antigravityHeaderScrub.ts";
|
||
import { antigravityIdeUserAgent } from "../../open-sse/services/antigravityHeaders.ts";
|
||
import {
|
||
clearAntigravityVersionCaches,
|
||
seedAntigravityIdeVersionCache,
|
||
seedAntigravityCliVersionCache,
|
||
} from "../../open-sse/services/antigravityVersion.ts";
|
||
import { clearAntigravityProjectCache } from "../../open-sse/services/antigravityProjectBootstrap.ts";
|
||
import { runWithCapture } from "../../open-sse/utils/providerRequestLogging.ts";
|
||
|
||
type AntigravityTransformResult = Exclude<
|
||
Awaited<ReturnType<AntigravityExecutor["transformRequest"]>>,
|
||
Response
|
||
>;
|
||
|
||
type ErrorPayload = {
|
||
error: {
|
||
code?: string;
|
||
message: string;
|
||
};
|
||
retryAfterMs?: number;
|
||
};
|
||
|
||
type ChatCompletionPayload = {
|
||
object?: string;
|
||
choices: Array<{
|
||
message: { content: string };
|
||
finish_reason: string;
|
||
}>;
|
||
usage?: {
|
||
prompt_tokens: number;
|
||
completion_tokens: number;
|
||
total_tokens: number;
|
||
};
|
||
};
|
||
|
||
async function withEnv<T>(
|
||
name: string,
|
||
value: string | undefined,
|
||
fn: () => T | Promise<T>
|
||
): Promise<T> {
|
||
const previous = process.env[name];
|
||
if (value === undefined) {
|
||
delete process.env[name];
|
||
} else {
|
||
process.env[name] = value;
|
||
}
|
||
|
||
try {
|
||
return await fn();
|
||
} finally {
|
||
if (previous === undefined) {
|
||
delete process.env[name];
|
||
} else {
|
||
process.env[name] = previous;
|
||
}
|
||
}
|
||
}
|
||
|
||
test.afterEach(() => {
|
||
clearAntigravityVersionCaches();
|
||
});
|
||
|
||
test("AntigravityExecutor.buildUrl always targets the streaming endpoint", () => {
|
||
const executor = new AntigravityExecutor();
|
||
assert.match(
|
||
executor.buildUrl("gemini-2.5-flash", true),
|
||
/\/v1internal:streamGenerateContent\?alt=sse$/
|
||
);
|
||
assert.equal(
|
||
executor.buildUrl("gemini-2.5-flash", false),
|
||
executor.buildUrl("gemini-2.5-flash", true)
|
||
);
|
||
});
|
||
|
||
test("AntigravityExecutor.buildHeaders includes native headers without OmniRoute internals", () => {
|
||
const executor = new AntigravityExecutor();
|
||
seedAntigravityIdeVersionCache("2.1.1");
|
||
const headers = executor.buildHeaders({ accessToken: "ag-token" }, false);
|
||
|
||
assert.equal(headers.Authorization, "Bearer ag-token");
|
||
assert.equal(headers.Accept, "text/event-stream");
|
||
assert.equal(headers["User-Agent"], antigravityIdeUserAgent("2.1.1"));
|
||
assert.equal(headers["X-OmniRoute-Source"], undefined);
|
||
});
|
||
|
||
test("Antigravity header scrub removes OmniRoute internal headers", () => {
|
||
const headers = scrubProxyAndFingerprintHeaders({
|
||
Authorization: "Bearer ag-token",
|
||
"X-OmniRoute-Source": "omniroute",
|
||
"X-OmniRoute-No-Cache": "true",
|
||
"X-Forwarded-For": "127.0.0.1",
|
||
});
|
||
|
||
assert.equal(headers.Authorization, "Bearer ag-token");
|
||
assert.equal(headers["X-OmniRoute-Source"], undefined);
|
||
assert.equal(headers["X-OmniRoute-No-Cache"], undefined);
|
||
assert.equal(headers["X-Forwarded-For"], undefined);
|
||
assert.equal(headers["Accept-Encoding"], "gzip, deflate, br");
|
||
});
|
||
|
||
test("AntigravityExecutor.transformRequest normalizes model, project and contents", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const body = {
|
||
request: {
|
||
contents: [
|
||
{
|
||
role: "model",
|
||
parts: [
|
||
{ thought: true, text: "skip me" },
|
||
{ thoughtSignature: "sig-only" },
|
||
{ text: "keep me" },
|
||
],
|
||
},
|
||
{
|
||
role: "model",
|
||
parts: [{ functionResponse: { name: "read_file", response: {} } }],
|
||
},
|
||
],
|
||
tools: [{ functionDeclarations: [{ name: "read_file" }] }],
|
||
},
|
||
};
|
||
|
||
const result = await executor.transformRequest("antigravity/gemini-3.1-pro", body, true, {
|
||
projectId: "project-1",
|
||
});
|
||
|
||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||
assert.equal(result.project, "project-1");
|
||
assert.equal(result.model, "gemini-3.1-pro");
|
||
assert.deepEqual(Object.keys(result), [
|
||
"project",
|
||
"requestId",
|
||
"request",
|
||
"model",
|
||
"userAgent",
|
||
"requestType",
|
||
]);
|
||
assert.equal(result.userAgent, "antigravity");
|
||
assert.match(result.requestId, /^agent\/\d+\/[0-9a-f]{8}$/);
|
||
assert.equal(result.enabledCreditTypes, undefined);
|
||
assert.ok(result.request.sessionId);
|
||
const request = result.request as { generationConfig?: { topK?: number; topP?: number } };
|
||
const generationConfig = request.generationConfig || {};
|
||
assert.equal(generationConfig.topK, 40);
|
||
assert.equal(generationConfig.topP, 1.0);
|
||
assert.deepEqual(result.request.toolConfig, {
|
||
functionCallingConfig: { mode: "VALIDATED" },
|
||
});
|
||
assert.deepEqual(result.request.contents[0].parts, [{ text: "keep me" }]);
|
||
assert.equal(result.request.contents[1].role, "user");
|
||
});
|
||
|
||
test("AntigravityExecutor.transformRequest strips thinking config for Cloud Code models that do not support reasoning", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const body = {
|
||
reasoning_effort: "high",
|
||
request: {
|
||
generationConfig: {
|
||
thinkingConfig: {
|
||
thinkingBudget: 8192,
|
||
includeThoughts: true,
|
||
},
|
||
},
|
||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||
},
|
||
};
|
||
|
||
const result = await executor.transformRequest("antigravity/claude-sonnet-4-6", body, true, {
|
||
projectId: "project-1",
|
||
});
|
||
|
||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||
const generationConfig = result.request.generationConfig as {
|
||
thinkingConfig?: { thinkingBudget?: number; includeThoughts?: boolean };
|
||
};
|
||
assert.equal(result.reasoning_effort, undefined);
|
||
assert.equal(generationConfig.thinkingConfig, undefined);
|
||
});
|
||
|
||
test("AntigravityExecutor.transformRequest preserves thinking config for gemini-pro-agent", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const body = {
|
||
request: {
|
||
generationConfig: {
|
||
thinkingConfig: {
|
||
thinkingBudget: 8192,
|
||
includeThoughts: true,
|
||
},
|
||
},
|
||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||
},
|
||
};
|
||
|
||
const result = await executor.transformRequest("antigravity/gemini-pro-agent", body, true, {
|
||
projectId: "project-1",
|
||
});
|
||
|
||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||
const generationConfig = result.request.generationConfig as {
|
||
thinkingConfig: { thinkingBudget?: number; includeThoughts?: boolean };
|
||
};
|
||
assert.equal(generationConfig.thinkingConfig.thinkingBudget, 8192);
|
||
assert.equal(generationConfig.thinkingConfig.includeThoughts, true);
|
||
});
|
||
|
||
test("AntigravityExecutor.transformRequest tolerates a missing body when projectId is present", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
|
||
const result = await executor.transformRequest("antigravity/gemini-3.1-pro", null, true, {
|
||
projectId: "project-1",
|
||
});
|
||
|
||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||
assert.equal(result.project, "project-1");
|
||
assert.equal(result.model, "gemini-3.1-pro");
|
||
assert.ok(result.request.sessionId);
|
||
});
|
||
|
||
test("AntigravityExecutor.transformRequest returns a structured error response when projectId is missing", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const result = await executor.transformRequest(
|
||
"gemini-2.5-flash",
|
||
{ request: { contents: [] } },
|
||
true,
|
||
{}
|
||
);
|
||
if (!(result instanceof Response)) throw new Error("Expected Response from transformRequest");
|
||
const payload = (await result.json()) as ErrorPayload;
|
||
|
||
assert.equal(result.status, 422);
|
||
assert.equal(payload.error.code, "missing_project_id");
|
||
assert.match(payload.error.message, /Missing Google projectId/);
|
||
});
|
||
|
||
// #2334/#2541: a freshly re-added Antigravity account can have an empty stored projectId
|
||
// even when its Google account already owns a Cloud Code project. transformRequest must
|
||
// auto-discover it via loadCodeAssist instead of hard-failing.
|
||
test("AntigravityExecutor.transformRequest auto-discovers a missing projectId via loadCodeAssist (#2334)", async () => {
|
||
clearAntigravityProjectCache();
|
||
seedAntigravityIdeVersionCache("2.1.1");
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
let loadCodeAssistCalled = false;
|
||
|
||
globalThis.fetch = (async (url: string | URL | Request) => {
|
||
if (String(url).includes("loadCodeAssist")) {
|
||
loadCodeAssistCalled = true;
|
||
return new Response(JSON.stringify({ cloudaicompanionProject: "discovered-project-123" }), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
}
|
||
return new Response("{}", { status: 404 });
|
||
}) as typeof fetch;
|
||
|
||
try {
|
||
const result = await executor.transformRequest(
|
||
"antigravity/gemini-3.1-pro",
|
||
{ request: { contents: [] } },
|
||
true,
|
||
{ accessToken: "fresh-account-token-2334" }
|
||
);
|
||
if (result instanceof Response) {
|
||
throw new Error(`Expected an envelope but got a ${result.status} Response`);
|
||
}
|
||
assert.equal(
|
||
loadCodeAssistCalled,
|
||
true,
|
||
"loadCodeAssist should be called to recover the project"
|
||
);
|
||
assert.equal(result.project, "discovered-project-123");
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
clearAntigravityProjectCache();
|
||
}
|
||
});
|
||
|
||
// #8491: when loadCodeAssist also finds no project and Google marks the
|
||
// account BYOP (no automatic project creation for standard-tier accounts),
|
||
// the fast 422 GCP_PROJECT_REQUIRED must be returned so the dashboard can
|
||
// prompt the user to enter a GCP Project ID.
|
||
test("AntigravityExecutor.transformRequest fast-422s with GCP_PROJECT_REQUIRED when loadCodeAssist finds no project (#8491)", async () => {
|
||
clearAntigravityProjectCache();
|
||
seedAntigravityIdeVersionCache("2.1.1");
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
|
||
globalThis.fetch = (async () =>
|
||
new Response(JSON.stringify({}), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
})) as typeof fetch;
|
||
|
||
try {
|
||
const result = await executor.transformRequest(
|
||
"antigravity/gemini-3.1-pro",
|
||
{ request: { contents: [] } },
|
||
true,
|
||
{ accessToken: "no-project-token-2334" }
|
||
);
|
||
if (!(result instanceof Response)) throw new Error("Expected a 422 Response");
|
||
assert.equal(result.status, 422);
|
||
const payload = (await result.json()) as ErrorPayload;
|
||
assert.equal(payload.error.code, "gcp_project_required");
|
||
assert.match(payload.error.message, /GCP_PROJECT_REQUIRED/);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
clearAntigravityProjectCache();
|
||
}
|
||
});
|
||
|
||
test("AntigravityExecutor.transformRequest prefers top-level credentials projectId over nested providerSpecificData", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const result = await executor.transformRequest(
|
||
"antigravity/gemini-2.5-pro",
|
||
{
|
||
project: "body-project",
|
||
request: {
|
||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||
},
|
||
},
|
||
true,
|
||
{
|
||
projectId: "credential-project",
|
||
providerSpecificData: { projectId: "nested-project" },
|
||
}
|
||
);
|
||
|
||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||
assert.equal(result.project, "credential-project");
|
||
});
|
||
|
||
test("AntigravityExecutor.transformRequest uses nested providerSpecificData projectId when top-level is absent", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const result = await executor.transformRequest(
|
||
"antigravity/gemini-2.5-pro",
|
||
{
|
||
request: {
|
||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||
},
|
||
},
|
||
true,
|
||
{
|
||
providerSpecificData: { projectId: "nested-project" },
|
||
}
|
||
);
|
||
|
||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||
assert.equal(result.project, "nested-project");
|
||
});
|
||
|
||
test("AntigravityExecutor.transformRequest treats whitespace-only project values as missing", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
|
||
const nestedFallback = await executor.transformRequest(
|
||
"antigravity/gemini-2.5-pro",
|
||
{
|
||
project: " ",
|
||
request: {
|
||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||
},
|
||
},
|
||
true,
|
||
{
|
||
projectId: " ",
|
||
providerSpecificData: { projectId: " nested-project " },
|
||
}
|
||
);
|
||
|
||
if (nestedFallback instanceof Response)
|
||
throw new Error("Unexpected Response from transformRequest");
|
||
assert.equal(nestedFallback.project, "nested-project");
|
||
|
||
const bodyFallback = await executor.transformRequest(
|
||
"antigravity/gemini-2.5-pro",
|
||
{
|
||
project: " body-project ",
|
||
request: {
|
||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||
},
|
||
},
|
||
true,
|
||
{
|
||
projectId: " ",
|
||
providerSpecificData: { projectId: " " },
|
||
}
|
||
);
|
||
|
||
if (bodyFallback instanceof Response)
|
||
throw new Error("Unexpected Response from transformRequest");
|
||
assert.equal(bodyFallback.project, "body-project");
|
||
});
|
||
|
||
test("AntigravityExecutor.transformRequest allows body project overrides when the env flag is enabled", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
|
||
await withEnv("OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE", "1", async () => {
|
||
const result = await executor.transformRequest(
|
||
"antigravity/gemini-2.5-pro",
|
||
{
|
||
project: "body-project",
|
||
request: {
|
||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||
sessionId: "session-fixed",
|
||
},
|
||
},
|
||
true,
|
||
{ projectId: "credential-project" }
|
||
);
|
||
|
||
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
|
||
assert.equal(result.project, "body-project");
|
||
assert.equal(result.request.sessionId, "session-fixed");
|
||
assert.equal(result.model, "gemini-2.5-pro");
|
||
});
|
||
});
|
||
|
||
test("AntigravityExecutor parses retry timing from headers and error strings", () => {
|
||
const executor = new AntigravityExecutor();
|
||
const headers = new Headers({
|
||
"retry-after": "120",
|
||
"x-ratelimit-reset-after": "30",
|
||
});
|
||
|
||
assert.equal(executor.parseRetryHeaders(headers), 120_000);
|
||
assert.equal(
|
||
executor.parseRetryFromErrorMessage("Your quota will reset after 2h7m23s"),
|
||
7_643_000
|
||
);
|
||
});
|
||
|
||
test("AntigravityExecutor.parseRetryHeaders falls back to reset-after and reset timestamps", () => {
|
||
const executor = new AntigravityExecutor();
|
||
const futureSeconds = Math.floor(Date.now() / 1000) + 90;
|
||
|
||
assert.equal(
|
||
executor.parseRetryHeaders(new Headers({ "x-ratelimit-reset-after": "45" })),
|
||
45_000
|
||
);
|
||
assert.ok(
|
||
executor.parseRetryHeaders(new Headers({ "x-ratelimit-reset": String(futureSeconds) })) >=
|
||
89_000
|
||
);
|
||
});
|
||
|
||
test("AntigravityExecutor.collectStreamToResponse turns SSE Gemini chunks into a chat completion", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const response = new Response(
|
||
[
|
||
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"Hello "}]},"finishReason":"STOP"}]}}\n\n',
|
||
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"world"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":3,"totalTokenCount":8}}}\n\n',
|
||
].join(""),
|
||
{
|
||
status: 200,
|
||
headers: { "Content-Type": "text/event-stream" },
|
||
}
|
||
);
|
||
|
||
const result = await executor.collectStreamToResponse(
|
||
response,
|
||
"gemini-2.5-flash",
|
||
"https://example.com",
|
||
{ Authorization: "Bearer ag-token" },
|
||
{ request: {} }
|
||
);
|
||
const payload = (await result.response.json()) as ChatCompletionPayload;
|
||
|
||
assert.equal(result.response.status, 200);
|
||
assert.equal(payload.object, "chat.completion");
|
||
assert.equal(payload.choices[0].message.content, "Hello world");
|
||
assert.equal(payload.choices[0].finish_reason, "stop");
|
||
assert.deepEqual(payload.usage, {
|
||
prompt_tokens: 5,
|
||
completion_tokens: 3,
|
||
total_tokens: 8,
|
||
});
|
||
});
|
||
|
||
test("AntigravityExecutor.collectStreamToResponse converts textual tool call SSE to structured tool_calls", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const response = new Response(
|
||
[
|
||
`data: ${JSON.stringify({
|
||
response: {
|
||
candidates: [
|
||
{
|
||
content: {
|
||
parts: [
|
||
{
|
||
text: '[Tool call: search_files]\nArguments: {"file_glob":"*gemini*","output_mode":"files_only","path":"/opt/O\\u200dmniRoute","target":"files"}',
|
||
},
|
||
],
|
||
},
|
||
finishReason: "STOP",
|
||
},
|
||
],
|
||
usageMetadata: {
|
||
promptTokenCount: 7,
|
||
candidatesTokenCount: 4,
|
||
totalTokenCount: 11,
|
||
},
|
||
},
|
||
})}\n\n`,
|
||
].join(""),
|
||
{
|
||
status: 200,
|
||
headers: { "Content-Type": "text/event-stream" },
|
||
}
|
||
);
|
||
|
||
const result = await executor.collectStreamToResponse(
|
||
response,
|
||
"gemini-3.5-flash-low",
|
||
"https://example.com",
|
||
{ Authorization: "Bearer ag-token" },
|
||
{ request: {} }
|
||
);
|
||
const payload = await result.response.json();
|
||
const choice = payload.choices[0];
|
||
|
||
assert.equal(choice.message.content, null);
|
||
assert.equal(choice.finish_reason, "tool_calls");
|
||
assert.equal(choice.message.tool_calls.length, 1);
|
||
assert.equal(choice.message.tool_calls[0].function.name, "search_files");
|
||
assert.deepEqual(JSON.parse(choice.message.tool_calls[0].function.arguments), {
|
||
file_glob: "*gemini*",
|
||
output_mode: "files_only",
|
||
path: "/opt/OmniRoute",
|
||
target: "files",
|
||
});
|
||
});
|
||
|
||
test("AntigravityExecutor.collectStreamToResponse parses fragmented SSE lines incrementally", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const encoder = new TextEncoder();
|
||
const streamText = [
|
||
`data: ${JSON.stringify({
|
||
response: {
|
||
candidates: [{ content: { parts: [{ text: "Frag" }] } }],
|
||
},
|
||
})}\n\n`,
|
||
`data: ${JSON.stringify({
|
||
response: {
|
||
candidates: [
|
||
{
|
||
content: { parts: [{ text: "mented" }] },
|
||
finishReason: "STOP",
|
||
},
|
||
],
|
||
usageMetadata: {
|
||
promptTokenCount: 9,
|
||
candidatesTokenCount: 4,
|
||
totalTokenCount: 13,
|
||
},
|
||
},
|
||
})}\n\n`,
|
||
].join("");
|
||
const response = new Response(
|
||
new ReadableStream({
|
||
start(controller) {
|
||
for (const chunk of [
|
||
streamText.slice(0, 6),
|
||
streamText.slice(6, 31),
|
||
streamText.slice(31, 79),
|
||
streamText.slice(79, 143),
|
||
streamText.slice(143),
|
||
]) {
|
||
controller.enqueue(encoder.encode(chunk));
|
||
}
|
||
controller.close();
|
||
},
|
||
}),
|
||
{
|
||
status: 200,
|
||
headers: { "Content-Type": "text/event-stream" },
|
||
}
|
||
);
|
||
|
||
const result = await executor.collectStreamToResponse(
|
||
response,
|
||
"gemini-2.5-flash",
|
||
"https://example.com",
|
||
{ Authorization: "Bearer ag-token" },
|
||
{ request: {} }
|
||
);
|
||
const payload = (await result.response.json()) as ChatCompletionPayload;
|
||
|
||
assert.equal(payload.choices[0].message.content, "Fragmented");
|
||
assert.equal(payload.choices[0].finish_reason, "stop");
|
||
assert.deepEqual(payload.usage, {
|
||
prompt_tokens: 9,
|
||
completion_tokens: 4,
|
||
total_tokens: 13,
|
||
});
|
||
});
|
||
|
||
test("AntigravityExecutor.refreshCredentials refreshes Google OAuth tokens", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = async (url) => {
|
||
assert.match(String(url), /oauth2\.googleapis\.com\/token$/);
|
||
return new Response(
|
||
JSON.stringify({
|
||
access_token: "new-token",
|
||
refresh_token: "new-refresh",
|
||
expires_in: 3600,
|
||
}),
|
||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||
);
|
||
};
|
||
|
||
try {
|
||
const result = await executor.refreshCredentials(
|
||
{ refreshToken: "refresh", projectId: "project-1" },
|
||
null
|
||
);
|
||
assert.deepEqual(result, {
|
||
accessToken: "new-token",
|
||
refreshToken: "new-refresh",
|
||
expiresIn: 3600,
|
||
projectId: "project-1",
|
||
providerSpecificData: undefined,
|
||
});
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("AntigravityExecutor.refreshCredentials discovers projectId when stored value is empty", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
clearAntigravityProjectCache();
|
||
|
||
let fetchCalls: string[] = [];
|
||
globalThis.fetch = async (url, init) => {
|
||
const urlStr = String(url);
|
||
fetchCalls.push(urlStr);
|
||
// Token refresh endpoint
|
||
if (urlStr.includes("oauth2.googleapis.com/token")) {
|
||
return new Response(
|
||
JSON.stringify({
|
||
access_token: "new-token",
|
||
refresh_token: "new-refresh",
|
||
expires_in: 3600,
|
||
}),
|
||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||
);
|
||
}
|
||
// loadCodeAssist endpoint - returns a projectId
|
||
if (urlStr.includes("loadCodeAssist")) {
|
||
return new Response(JSON.stringify({ cloudaicompanionProject: "discovered-project" }), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
}
|
||
return new Response("not found", { status: 404 });
|
||
};
|
||
|
||
try {
|
||
const result = await executor.refreshCredentials(
|
||
{ refreshToken: "refresh", projectId: "", connectionId: "conn-1" },
|
||
null
|
||
);
|
||
assert.equal(result?.projectId, "discovered-project");
|
||
assert.equal(result?.accessToken, "new-token");
|
||
assert.equal(result?.refreshToken, "new-refresh");
|
||
assert.equal(result?.expiresIn, 3600);
|
||
assert.ok(
|
||
fetchCalls.some((u) => u.includes("oauth2.googleapis.com/token")),
|
||
"should call token endpoint"
|
||
);
|
||
assert.ok(
|
||
fetchCalls.some((u) => u.includes("loadCodeAssist")),
|
||
"should call loadCodeAssist"
|
||
);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
clearAntigravityProjectCache();
|
||
}
|
||
});
|
||
|
||
test("AntigravityExecutor.refreshCredentials skips discovery when projectId already set", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
clearAntigravityProjectCache();
|
||
|
||
let fetchCalls: string[] = [];
|
||
globalThis.fetch = async (url) => {
|
||
const urlStr = String(url);
|
||
fetchCalls.push(urlStr);
|
||
if (urlStr.includes("oauth2.googleapis.com/token")) {
|
||
return new Response(
|
||
JSON.stringify({
|
||
access_token: "new-token",
|
||
refresh_token: "new-refresh",
|
||
expires_in: 3600,
|
||
}),
|
||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||
);
|
||
}
|
||
return new Response("not found", { status: 404 });
|
||
};
|
||
|
||
try {
|
||
const result = await executor.refreshCredentials(
|
||
{ refreshToken: "refresh", projectId: "existing-project" },
|
||
null
|
||
);
|
||
assert.equal(result?.projectId, "existing-project");
|
||
assert.ok(
|
||
!fetchCalls.some((u) => u.includes("loadCodeAssist")),
|
||
"should NOT call loadCodeAssist"
|
||
);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
clearAntigravityProjectCache();
|
||
}
|
||
});
|
||
|
||
test("AntigravityExecutor.refreshCredentials handles discovery failure gracefully", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
clearAntigravityProjectCache();
|
||
|
||
globalThis.fetch = async (url) => {
|
||
const urlStr = String(url);
|
||
if (urlStr.includes("oauth2.googleapis.com/token")) {
|
||
return new Response(
|
||
JSON.stringify({
|
||
access_token: "new-token",
|
||
refresh_token: "new-refresh",
|
||
expires_in: 3600,
|
||
}),
|
||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||
);
|
||
}
|
||
// loadCodeAssist fails
|
||
if (urlStr.includes("loadCodeAssist")) {
|
||
return new Response("server error", { status: 500 });
|
||
}
|
||
return new Response("not found", { status: 404 });
|
||
};
|
||
|
||
try {
|
||
const result = await executor.refreshCredentials(
|
||
{ refreshToken: "refresh", projectId: "", connectionId: "conn-1" },
|
||
null
|
||
);
|
||
// Discovery failed, projectId stays empty but refresh still succeeds
|
||
assert.equal(result?.projectId, "");
|
||
assert.equal(result?.accessToken, "new-token");
|
||
assert.equal(result?.refreshToken, "new-refresh");
|
||
assert.equal(result?.expiresIn, 3600);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
clearAntigravityProjectCache();
|
||
}
|
||
});
|
||
|
||
test("AntigravityExecutor.refreshCredentials skips discovery when access_token is not a string", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
clearAntigravityProjectCache();
|
||
|
||
let fetchCalls: string[] = [];
|
||
globalThis.fetch = async (url) => {
|
||
const urlStr = String(url);
|
||
fetchCalls.push(urlStr);
|
||
if (urlStr.includes("oauth2.googleapis.com/token")) {
|
||
// access_token missing from response
|
||
return new Response(JSON.stringify({ refresh_token: "new-refresh", expires_in: 3600 }), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
}
|
||
return new Response("not found", { status: 404 });
|
||
};
|
||
|
||
try {
|
||
const result = await executor.refreshCredentials(
|
||
{ refreshToken: "refresh", projectId: "", connectionId: "conn-1" },
|
||
null
|
||
);
|
||
assert.equal(result?.projectId, "");
|
||
assert.equal(result?.accessToken, undefined);
|
||
assert.ok(
|
||
!fetchCalls.some((u) => u.includes("loadCodeAssist")),
|
||
"should NOT call loadCodeAssist"
|
||
);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
clearAntigravityProjectCache();
|
||
}
|
||
});
|
||
|
||
// The non-streaming passthrough drain test ("auto-retries short 429 responses and
|
||
// collects SSE for non-stream clients") lives in
|
||
// tests/unit/antigravity-streaming-passthrough.test.ts with the other passthrough tests.
|
||
|
||
test("AntigravityExecutor.execute embeds retryAfterMs when the upstream asks for a long wait", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
seedAntigravityIdeVersionCache("2.1.1");
|
||
|
||
globalThis.fetch = async () =>
|
||
new Response(
|
||
JSON.stringify({
|
||
error: {
|
||
message: "Your quota will reset after 2h",
|
||
},
|
||
}),
|
||
{
|
||
status: 429,
|
||
headers: { "Content-Type": "application/json" },
|
||
}
|
||
);
|
||
|
||
try {
|
||
const result = await executor.execute({
|
||
model: "antigravity/gemini-2.5-flash",
|
||
body: { request: { contents: [] } },
|
||
stream: true,
|
||
credentials: { accessToken: "token", projectId: "project-1" },
|
||
log: { debug() {}, warn() {} },
|
||
});
|
||
const payload = (await result.response.json()) as ErrorPayload;
|
||
|
||
assert.equal(result.response.status, 429);
|
||
assert.equal(payload.retryAfterMs, 7_200_000);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("AntigravityExecutor.execute bounds a persistent short-retry 429 instead of looping forever", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
const originalSetTimeout = globalThis.setTimeout;
|
||
const calls: string[] = [];
|
||
seedAntigravityIdeVersionCache("2.1.1");
|
||
|
||
// "rate limited" with no parseable retry hint classifies as rate_limited →
|
||
// decide429 returns short_cooldown_switch_auth (60s default). Since #9351 the
|
||
// switchAuth signal is propagated to the retry guard, which declines the
|
||
// same-URL sleep branch entirely: one attempt per base URL, then the 429 is
|
||
// returned so the account-fallback layer can switch accounts immediately
|
||
// instead of sleeping 60s × MAX_AUTO_RETRIES against a rate-limited account.
|
||
globalThis.fetch = async (url) => {
|
||
calls.push(String(url));
|
||
return new Response(JSON.stringify({ error: { message: "rate limited" } }), {
|
||
status: 429,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
};
|
||
globalThis.setTimeout = ((callback) => {
|
||
(callback as () => void)();
|
||
return 0;
|
||
}) as typeof setTimeout;
|
||
|
||
try {
|
||
const result = await executor.execute({
|
||
model: "antigravity/gemini-2.5-flash",
|
||
body: { request: { contents: [] } },
|
||
stream: true,
|
||
credentials: { accessToken: "token", projectId: "project-1" },
|
||
log: { debug() {}, warn() {} },
|
||
});
|
||
|
||
// Returns the 429 rather than hanging.
|
||
assert.equal(result.response.status, 429);
|
||
|
||
// Bounded: switchAuth declines same-URL retries → 2 live runtime endpoints
|
||
// × 1 attempt each = 2 attempts total (#9351).
|
||
assert.equal(calls.length, 2);
|
||
|
||
// Tried every distinct live runtime base URL before giving up.
|
||
const distinctHosts = new Set(calls.map((u) => new URL(u).host));
|
||
assert.equal(distinctHosts.size, 2);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
globalThis.setTimeout = originalSetTimeout;
|
||
}
|
||
});
|
||
|
||
test("AntigravityExecutor.execute aborts during project bootstrap without starting runtime fetch", async () => {
|
||
clearAntigravityProjectCache();
|
||
seedAntigravityIdeVersionCache("2026.04.17-test");
|
||
seedAntigravityCliVersionCache("2026.04.17-test");
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
const controller = new AbortController();
|
||
const calls: string[] = [];
|
||
|
||
globalThis.fetch = async (url, init) => {
|
||
calls.push(String(url));
|
||
assert.match(String(url), /:loadCodeAssist$/);
|
||
return new Promise<Response>((_resolve, reject) => {
|
||
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true });
|
||
});
|
||
};
|
||
|
||
try {
|
||
const pending = executor.execute({
|
||
model: "antigravity/gemini-2.5-flash",
|
||
body: { request: { contents: [] } },
|
||
stream: true,
|
||
credentials: { accessToken: "bootstrap-abort-token" },
|
||
signal: controller.signal,
|
||
log: { debug() {}, warn() {} },
|
||
});
|
||
while (calls.length === 0) {
|
||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||
}
|
||
controller.abort(new DOMException("caller disconnected", "AbortError"));
|
||
|
||
await assert.rejects(pending, { name: "AbortError" });
|
||
assert.equal(calls.length, 1);
|
||
assert.equal(
|
||
calls.some((url) => url.includes("streamGenerateContent")),
|
||
false
|
||
);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
clearAntigravityProjectCache();
|
||
}
|
||
});
|
||
|
||
test("AntigravityExecutor.execute tags pre-response stalls with a fallbackable timeout code", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
const originalSetTimeout = globalThis.setTimeout;
|
||
seedAntigravityIdeVersionCache("2.1.1");
|
||
|
||
globalThis.fetch = async (_url, init) => {
|
||
await new Promise((_resolve, reject) => {
|
||
const signal = init?.signal as AbortSignal | undefined;
|
||
if (signal?.aborted) {
|
||
reject(signal.reason);
|
||
return;
|
||
}
|
||
signal?.addEventListener("abort", () => reject(signal.reason), { once: true });
|
||
});
|
||
throw new Error("unreachable");
|
||
};
|
||
globalThis.setTimeout = ((callback) => {
|
||
(callback as () => void)();
|
||
return 0;
|
||
}) as typeof setTimeout;
|
||
|
||
try {
|
||
await assert.rejects(
|
||
() =>
|
||
executor.execute({
|
||
model: "antigravity/gemini-2.5-flash",
|
||
body: { request: { contents: [] } },
|
||
stream: true,
|
||
credentials: { accessToken: "token", projectId: "project-1" },
|
||
log: { debug() {}, warn() {}, error() {} },
|
||
}),
|
||
(error: unknown) => {
|
||
assert.equal((error as { code?: string }).code, "ANTIGRAVITY_PRE_RESPONSE_TIMEOUT");
|
||
assert.equal((error as { name?: string }).name, "TimeoutError");
|
||
assert.match((error as Error).message, /did not return response headers/);
|
||
return true;
|
||
}
|
||
);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
globalThis.setTimeout = originalSetTimeout;
|
||
}
|
||
});
|
||
|
||
test("AntigravityExecutor.execute applies CLI fingerprint when enabled", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const originalFetch = globalThis.fetch;
|
||
let fetchStarted = false;
|
||
let fetchBody: Record<string, unknown> | null = null;
|
||
let prepared: unknown = null;
|
||
let preparedBeforeFetch = false;
|
||
seedAntigravityIdeVersionCache("2.1.1");
|
||
setCliCompatProviders(["antigravity"]);
|
||
|
||
globalThis.fetch = async (_url, init) => {
|
||
fetchStarted = true;
|
||
const headers = init?.headers as Record<string, string>;
|
||
const parsedBody = JSON.parse(String(init?.body));
|
||
fetchBody = parsedBody;
|
||
|
||
assert.equal(headers["User-Agent"], antigravityIdeUserAgent("2.1.1"));
|
||
assert.equal(headers["x-client-name"], undefined);
|
||
assert.equal(headers["x-client-version"], undefined);
|
||
assert.equal(headers["x-goog-user-project"], "project-1");
|
||
assert.deepEqual(Object.keys(parsedBody), [
|
||
"project",
|
||
"requestId",
|
||
"request",
|
||
"model",
|
||
"userAgent",
|
||
"requestType",
|
||
"enabledCreditTypes",
|
||
]);
|
||
assert.deepEqual(parsedBody.enabledCreditTypes, ["GOOGLE_ONE_AI"]);
|
||
|
||
return new Response(
|
||
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"OK"}]},"finishReason":"STOP"}]}}\n\n',
|
||
{
|
||
status: 200,
|
||
headers: { "Content-Type": "text/event-stream" },
|
||
}
|
||
);
|
||
};
|
||
|
||
try {
|
||
const requestCapture = {
|
||
capture(request) {
|
||
preparedBeforeFetch = !fetchStarted;
|
||
prepared = request.body;
|
||
},
|
||
body(fallback) {
|
||
return prepared ?? fallback;
|
||
},
|
||
latest() {
|
||
return null;
|
||
},
|
||
};
|
||
const result = await withEnv("ANTIGRAVITY_CREDITS", "always", () =>
|
||
runWithCapture(requestCapture, () =>
|
||
executor.execute({
|
||
model: "antigravity/gemini-2.5-flash",
|
||
body: { request: { contents: [] } },
|
||
stream: false,
|
||
credentials: { accessToken: "token", projectId: "project-1" },
|
||
log: { debug() {}, warn() {}, info() {} },
|
||
})
|
||
)
|
||
);
|
||
|
||
assert.equal(result.response.status, 200);
|
||
assert.equal(preparedBeforeFetch, true);
|
||
assert.deepEqual(prepared, fetchBody);
|
||
} finally {
|
||
setCliCompatProviders([]);
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("AntigravityExecutor.transformRequest maps Claude models through Gemini contents schema", async () => {
|
||
const executor = new AntigravityExecutor();
|
||
const body = {
|
||
project: "project-1",
|
||
model: "claude-sonnet-4-6",
|
||
userAgent: "antigravity",
|
||
requestId: "agent-123",
|
||
requestType: "agent",
|
||
request: {
|
||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||
systemInstruction: { role: "system", parts: [{ text: "System prompt" }] },
|
||
generationConfig: {
|
||
temperature: 1,
|
||
maxOutputTokens: 16384,
|
||
},
|
||
messages: [{ role: "user", content: [{ type: "text", text: "Legacy Anthropic field" }] }],
|
||
system: [{ type: "text", text: "Legacy system field" }],
|
||
max_tokens: 16384,
|
||
stream: true,
|
||
temperature: 1,
|
||
},
|
||
};
|
||
|
||
const result = (await executor.transformRequest("antigravity/claude-sonnet-4-6", body, true, {
|
||
projectId: "project-1",
|
||
})) as AntigravityTransformResult;
|
||
|
||
assert.equal(result.project, "project-1");
|
||
assert.equal(result.model, "claude-sonnet-4-6");
|
||
assert.equal(result.requestType, "agent");
|
||
assert.ok(result.request.sessionId);
|
||
assert.equal(result.enabledCreditTypes, undefined);
|
||
assert.deepEqual(result.request.contents, [{ role: "user", parts: [{ text: "Hello" }] }]);
|
||
assert.deepEqual(result.request.systemInstruction, {
|
||
role: "system",
|
||
parts: [{ text: "System prompt" }],
|
||
});
|
||
assert.deepEqual(result.request.generationConfig, {
|
||
temperature: 1,
|
||
maxOutputTokens: 16384,
|
||
topK: 40,
|
||
topP: 1.0,
|
||
});
|
||
assert.equal(result.request.messages, undefined);
|
||
assert.equal(result.request.system, undefined);
|
||
assert.equal(result.request.max_tokens, undefined);
|
||
assert.equal(result.request.stream, undefined);
|
||
assert.equal(result.request.temperature, undefined);
|
||
assert.equal(result.request.toolConfig, undefined);
|
||
});
|