fix(providers): eliminate Gemini 3.5 Flash

This commit is contained in:
backryun
2026-08-23 22:46:54 +09:00
committed by Markus Hartung
parent 29caad9f3d
commit 2764812ee4
34 changed files with 166 additions and 218 deletions

View File

@@ -1 +0,0 @@
- fix(sse): mark gemini-3.5-flash as thinking-capable so reasoning_effort is no longer rejected with a spurious 400 (#10286)

View File

@@ -6,7 +6,7 @@ lastUpdated: 2026-07-31
# OmniRoute Antigravity (Google One AI) Onboarding Guide
> **What you get**: Access to Gemini 3.1 Pro, Gemini 3.5 Flash, Claude Sonnet 4.6, and other models through your Google One AI Pro subscription — routed through OmniRoute as a unified gateway.
> **What you get**: Access to Gemini 3.1 Pro, Gemini 3.7 Flash, Claude Sonnet 4.6, and other models through your Google One AI Pro subscription — routed through OmniRoute as a unified gateway.
**Official references**:
@@ -45,7 +45,7 @@ Both providers share the **same Google backend** — identical OAuth client, tok
**Why the model catalog differs**: Google's CLI is "optimized for speed and low overhead" and "co-optimized with Gemini models" (per Google's official blog). The Web/IDE product is "optimized for comprehensiveness." The CLI uses `:fetchAvailableModels` to dynamically discover models, while the IDE uses a static curated list.
**In practice**: Use `agy/` prefix for Gemini models (e.g. `agy/gemini-3.5-flash-high`). Use `antigravity/` for the static curated list. Both hit the same Google backend, but expose different model naming. The quota is shared — using either provider counts against the same Google account's limits.
**In practice**: Use `agy/` prefix for Gemini models (e.g. `agy/gemini-3.7-flash-high`). Use `antigravity/` for the static curated list. Both hit the same Google backend, but expose different model naming. The quota is shared — using either provider counts against the same Google account's limits.
---

View File

@@ -113,6 +113,7 @@ const AGY_RETIRED_MODEL_IDS = new Set([
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3-flash-agent",
"gemini-3.5-flash",
"gemini-3.5-flash-extra-low",
"gemini-3.5-flash-low",
"gemini-3.5-flash-high",

View File

@@ -179,6 +179,7 @@ const ANTIGRAVITY_RETIRED_MODEL_IDS = new Set([
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3-flash-agent",
"gemini-3.5-flash",
"gemini-3.5-flash-extra-low",
"gemini-3.5-flash-low",
"gemini-3.5-flash-high",

View File

@@ -8,7 +8,6 @@
"gemma-4-26b-it": { "rpm": 16000, "rpd": 14400, "tpm": 16000 },
"gemma-4-31b-it": { "rpm": 16000, "rpd": 14400, "tpm": 16000 },
"gemini-embedding-exp-03-07": { "rpm": 100, "rpd": 1000, "tpm": 30000 },
"gemini-3.5-flash": { "rpm": 5, "rpd": 20, "tpm": 250000 },
"gemini-3.1-flash-lite": { "rpm": 15, "rpd": 500, "tpm": 250000 },
"gemini-3.1-pro": { "rpm": 0, "rpd": 0, "tpm": 0 },
"gemini-2.5-flash-lite": { "rpm": 10, "rpd": 20, "tpm": 250000 },

View File

@@ -228,14 +228,14 @@ export const cursorProvider: RegistryEntry = {
{ id: "gpt-5.1-low", name: "GPT-5.1 Low" },
{ id: "gpt-5.1", name: "GPT-5.1" },
{ id: "gpt-5.1-high", name: "GPT-5.1 High" },
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash" },
{ id: "claude-4-sonnet", name: "Sonnet 4" },
{ id: "claude-4-sonnet-thinking", name: "Sonnet 4 Thinking" },
{ id: "gpt-5-mini", name: "GPT-5 Mini" },
{ id: "kimi-k3-low", name: "Kimi K3 Low" },
{ id: "kimi-k3-max", name: "Kimi K3" },
{ id: "glm-5.2-high", name: "GLM 5.2" },
{ id: "glm-5.2-max", name: "GLM 5.2 Max" }, ],
{ id: "glm-5.2-max", name: "GLM 5.2 Max" },
],
};
/**

View File

@@ -78,7 +78,7 @@ const FALLBACK_MODEL_SEEDS: FallbackModelSeed[] = [
/** Presets exposed by the web client's model picker (id → text/multimodal model). */
export const CONOL_FALLBACK_MODEL_PRESETS: ConolModelPreset[] = [
{ id: "flash", text: "deepseek/deepseek-v4-flash", multimodal: "google/gemini-3.5-flash" },
{ id: "flash", text: "deepseek/deepseek-v4-flash", multimodal: "google/gemini-3.7-flash" },
{ id: "moderate", text: "deepseek/deepseek-v4-pro", multimodal: "claude-sonnet-5" },
{ id: "pro", text: "z-ai/glm-5.2", multimodal: "moonshotai/kimi-k3" },
{ id: "ultra", text: "claude-fable-5", multimodal: "claude-fable-5" },

View File

@@ -6,7 +6,7 @@
*/
export interface PromptQlModel {
/** Client-facing id (model_reference slug, e.g. gemini-3.5-flash). */
/** Client-facing id (model_reference slug, e.g. gemini-3.7-flash). */
id: string;
/** Friendly picker label. */
name: string;

View File

@@ -34,6 +34,8 @@ const IGNORED_METHODS = new Set([
"asyncBatchEmbedContent",
]);
const RETIRED_GEMINI_MODEL_IDS = new Set(["gemini-3.5-flash"]);
export interface GeminiDiscoveryModel {
id: string;
name: string;
@@ -46,36 +48,38 @@ export interface GeminiDiscoveryModel {
}
export function parseGeminiModelsList(data: any): GeminiDiscoveryModel[] {
return (data?.models || []).map((m: Record<string, unknown>) => {
const methods: string[] = Array.isArray(m.supportedGenerationMethods)
? (m.supportedGenerationMethods as string[])
: [];
return (data?.models || [])
.map((m: Record<string, unknown>) => {
const methods: string[] = Array.isArray(m.supportedGenerationMethods)
? (m.supportedGenerationMethods as string[])
: [];
const endpoints = new Set<string>(
methods
.filter((method) => !IGNORED_METHODS.has(method))
.map((method) => METHOD_TO_ENDPOINT[method] || "chat")
);
const endpoints = new Set<string>(
methods
.filter((method) => !IGNORED_METHODS.has(method))
.map((method) => METHOD_TO_ENDPOINT[method] || "chat")
);
const id = ((m.name as string) || (m.id as string) || "").replace(/^models\//, "");
const lowerId = id.toLowerCase();
const id = ((m.name as string) || (m.id as string) || "").replace(/^models\//, "");
const lowerId = id.toLowerCase();
// Keep Veo models in the video bucket even when the method list is incomplete.
if (lowerId.includes("veo")) {
endpoints.add("video");
}
// Keep Veo models in the video bucket even when the method list is incomplete.
if (lowerId.includes("veo")) {
endpoints.add("video");
}
if (endpoints.size === 0) endpoints.add("chat");
if (endpoints.size === 0) endpoints.add("chat");
return {
...m,
id,
name: (m.displayName as string) || id,
supportedEndpoints: [...endpoints],
...(typeof m.inputTokenLimit === "number" ? { inputTokenLimit: m.inputTokenLimit } : {}),
...(typeof m.outputTokenLimit === "number" ? { outputTokenLimit: m.outputTokenLimit } : {}),
...(typeof m.description === "string" ? { description: m.description } : {}),
...(m.thinking === true ? { supportsThinking: true } : {}),
} as GeminiDiscoveryModel;
});
return {
...m,
id,
name: (m.displayName as string) || id,
supportedEndpoints: [...endpoints],
...(typeof m.inputTokenLimit === "number" ? { inputTokenLimit: m.inputTokenLimit } : {}),
...(typeof m.outputTokenLimit === "number" ? { outputTokenLimit: m.outputTokenLimit } : {}),
...(typeof m.description === "string" ? { description: m.description } : {}),
...(m.thinking === true ? { supportsThinking: true } : {}),
} as GeminiDiscoveryModel;
})
.filter((model: GeminiDiscoveryModel) => !RETIRED_GEMINI_MODEL_IDS.has(model.id));
}

View File

@@ -20,7 +20,9 @@ import { normalizeDiscoveredModels } from "@/lib/providerModels/modelDiscovery";
import {
ANTIGRAVITY_MODEL_ALIASES,
ANTIGRAVITY_REVERSE_MODEL_ALIASES,
isDiscoverableAntigravityModelId,
} from "@omniroute/open-sse/config/antigravityModelAliases.ts";
import { isDiscoverableAgyModelId } from "@omniroute/open-sse/config/agyModels.ts";
import { filterChatSelectableModels } from "@omniroute/open-sse/services/modelEndpointPolicy.ts";
import { filterSelectableModels } from "@omniroute/open-sse/services/modelLifecycle.ts";
import { isSelfHostedChatProvider } from "@/shared/constants/providers";
@@ -254,14 +256,22 @@ export async function importManagedModels({
const previousSyncedAvailableModels =
previousSyncedAvailableModelsInput ??
(await getSyncedAvailableModelsForConnection(providerId, connectionId));
const normalizedDiscoveredModels = normalizeDiscoveredModels(fetchedModels, providerId);
// Gemini 3.5 Flash elimination (ddf1bb760, carried from #11259): antigravity/
// agy discovery is restricted to each family's discoverable ids BEFORE any
// chat-selection filtering.
const providerFilteredModels =
providerId === "antigravity"
? normalizedDiscoveredModels.filter((model) => isDiscoverableAntigravityModelId(model.id))
: providerId === "agy"
? normalizedDiscoveredModels.filter((model) => isDiscoverableAgyModelId(model.id))
: normalizedDiscoveredModels;
// #11088 (option 1): self-hosted providers keep their non-chat models — chat
// filtering happens at read time (resolveLocalSyncedEndpointRoute). Every other
// provider keeps the import-time chat filter: the read-time path is gated on
// isSelfHostedChatProvider, so dropping it globally leaked image/video models
// into OpenAI chat selections (#11271).
const selectableModels = filterSelectableModels(
providerId,
normalizeDiscoveredModels(fetchedModels, providerId)
const selectableModels = filterSelectableModels(providerId, providerFilteredModels);
);
const discoveredModels = isSelfHostedChatProvider(providerId)
? selectableModels

View File

@@ -99,7 +99,7 @@ const GPT_5_6_MODEL_SPEC = {
supportsVision: true,
} satisfies ModelSpec;
const GEMINI_35_FLASH_MODEL_SPEC = {
const GEMINI_36_FLASH_MODEL_SPEC = {
maxOutputTokens: 65536,
contextWindow: 1048576,
supportsThinking: false,
@@ -160,7 +160,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
aliases: ["openai/gpt-4o"],
},
// ── Gemini 2.5 and provider-neutral 3.5 Flash series ─────────────
// ── Gemini 2.5 Flash ─────────────────────────────────────────────
"gemini-2.5-flash": {
maxOutputTokens: 65536,
contextWindow: 1048576,
@@ -171,16 +171,6 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
supportsTools: true,
supportsVision: true,
},
"gemini-3.5-flash-extra-low": {
...GEMINI_35_FLASH_MODEL_SPEC,
thinkingBudgetCap: 0,
},
"gemini-3.5-flash-low": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3-flash-agent": {
...GEMINI_35_FLASH_MODEL_SPEC,
thinkingBudgetCap: 0,
},
// ── Gemini 3.7 Flash (current Antigravity/AGY live tiers) ─────────
// The tier suffix configures the thinking budget passed to the upstream
// gemini-3.7-flash-tiered backend (high: 24.5k, medium: 8k, low: 1k).
@@ -234,9 +224,9 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
// Provider-neutral compatibility for providers that still serve Gemini 3.6.
// Antigravity/AGY availability is governed by their own provider catalogs and
// retirement filters; these shared specs must not be treated as an allowlist.
"gemini-3.6-flash-high": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.6-flash-medium": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.6-flash-low": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.6-flash-high": { ...GEMINI_36_FLASH_MODEL_SPEC },
"gemini-3.6-flash-medium": { ...GEMINI_36_FLASH_MODEL_SPEC },
"gemini-3.6-flash-low": { ...GEMINI_36_FLASH_MODEL_SPEC },
// ── Gemini 3 Flash series ───────────────────────────────────────
"gemini-3-flash": {
@@ -282,20 +272,6 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
aliases: ["gemini-3-pro-low"],
},
// ── Gemini 3.5 Flash ─────────────────────────────────────────────
// #10286: the base Google AI Studio model DOES support reasoning (it has
// an effort-tier alias gemini-3.5-flash-high) — override the shared spec's
// supportsThinking:false here only. Do NOT flip GEMINI_35_FLASH_MODEL_SPEC
// itself: it is also spread into the Antigravity flash-tier aliases
// (gemini-3.5-flash-low/-extra-low, gemini-3-flash-agent, gemini-3.6-flash-*)
// which reject client-supplied thinking params because the model id itself
// selects the reasoning tier upstream.
"gemini-3.5-flash": {
...GEMINI_35_FLASH_MODEL_SPEC,
supportsThinking: true,
aliases: ["gemini-3.5-flash-high"],
},
// ── Claude Opus 4.5 ─────────────────────────────────────────────
"claude-opus-4-5": {
maxOutputTokens: 32768,

View File

@@ -57,6 +57,7 @@ test("agy ships its own live callable model catalog", () => {
assert.ok(!ids.includes("gemini-3.6-flash-low"));
assert.ok(!ids.includes("gemini-3.6-flash-medium"));
assert.ok(!ids.includes("gemini-3.6-flash-high"));
assert.ok(!ids.includes("gemini-3.5-flash"));
assert.ok(!ids.includes("gemini-3.5-flash-extra-low"));
assert.ok(!ids.includes("gemini-3.5-flash-low"));
assert.ok(!ids.includes("gemini-3-flash-agent"));
@@ -87,6 +88,7 @@ test("agy model helpers resolve catalog ids and display names", () => {
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-low"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-medium"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.6-flash-high"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-extra-low"), false);
assert.equal(isUserCallableAgyModelId("gemini-3.5-flash-low"), false);
assert.equal(isUserCallableAgyModelId("gemini-3-flash-agent"), false);

View File

@@ -74,7 +74,7 @@ test("TDD S3: checkFallbackError extracts retry hint for oauth providers even if
429,
errorText,
0,
"gemini-3.5-flash",
"gemini-3.7-flash",
"antigravity", // which uses oauth provider profile (useUpstreamRetryHints: false)
null
);

View File

@@ -31,6 +31,7 @@ const RETIRED_FLASH_IDS = [
"gemini-3.6-flash-low",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-high",
"gemini-3.5-flash",
"gemini-3.5-flash-extra-low",
"gemini-3.5-flash-low",
"gemini-3-flash-agent",

View File

@@ -21,6 +21,7 @@ const RETIRED_PUBLIC_MODELS = [
"gemini-3.6-flash-medium",
"gemini-3.6-flash-low",
"gemini-3-flash-agent",
"gemini-3.5-flash",
"gemini-3.5-flash-low",
"gemini-3.5-flash-extra-low",
"gemini-2.5-pro",

View File

@@ -14,6 +14,7 @@ import {
} from "../../open-sse/executors/conol-web.ts";
import {
CONOL_FALLBACK_MODELS,
CONOL_FALLBACK_MODEL_PRESETS,
clampConolEffort,
parseConolAgentServers,
resolveConolModelSelection,
@@ -27,6 +28,11 @@ import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts
const SESSION_COOKIE_NAME = "__Secure-better-auth.session_token";
describe("Conol web provider", () => {
it("routes the Flash preset multimodal path to Gemini 3.7", () => {
const flashPreset = CONOL_FALLBACK_MODEL_PRESETS.find((preset) => preset.id === "flash");
assert.equal(flashPreset?.multimodal, "google/gemini-3.7-flash");
});
it("normalizes raw, full-header, JSON, and provider-data credentials", () => {
assert.equal(normalizeConolCookie("token-value"), `${SESSION_COOKIE_NAME}=token-value`);
assert.equal(

View File

@@ -55,7 +55,7 @@ describe("GithubExecutor — Gemini/Claude must never hit /responses (port 9rout
it("routes registered Gemini Copilot models to chat/completions", () => {
const exec = new GithubExecutor();
for (const id of ["gemini-3.1-pro-preview", "gemini-3.5-flash"]) {
for (const id of ["gemini-3.1-pro-preview", "gemini-3.7-flash"]) {
assert.equal(exec.buildUrl(id, false), CHAT_URL, `${id} must route to chat/completions`);
}
});

View File

@@ -8,6 +8,10 @@ function modelIds(): Set<string> {
return new Set(cursorProvider.models.map((m) => m.id));
}
test("cursor registry excludes retired Gemini 3.5 Flash", () => {
assert.equal(modelIds().has("gemini-3.5-flash"), false);
});
test("cursor registry includes Claude Opus 4.8 effort + thinking + fast variants", () => {
const ids = modelIds();
for (const effort of EFFORTS) {

View File

@@ -56,7 +56,7 @@ describe("PromptQl — registry consistency", () => {
it("registers a model catalog via getModelsByProviderId", () => {
const catalog = getModelsByProviderId("promptql");
assert.ok(catalog.length >= 5);
assert.ok(catalog.some((m) => m.id === "gemini-3.5-flash" || m.id.includes("gemini")));
assert.ok(catalog.some((m) => m.id === "gemini-3.7-flash" || m.id.includes("gemini")));
assert.ok(catalog.some((m) => m.id.includes("gpt-5.6") || m.id.includes("fable")));
});
});
@@ -209,7 +209,10 @@ describe("PromptQl — helpers", () => {
});
it("resolves model slugs and prefixes", () => {
assert.equal(models.clientFacingPromptQlModelId("promptql/gemini-3.5-flash"), "gemini-3.5-flash");
assert.equal(
models.clientFacingPromptQlModelId("promptql/gemini-3.7-flash"),
"gemini-3.7-flash"
);
assert.equal(models.clientFacingPromptQlModelId("pql/gpt-5.6-sol"), "gpt-5.6-sol");
const r = models.resolvePromptQlModel("Claude Fable 5");
assert.ok(r);
@@ -265,7 +268,7 @@ describe("PromptQlExecutor — auth / validation", () => {
it("returns 401 when no token is supplied", async () => {
const executor = new mod.PromptQlExecutor();
const result = await executor.execute({
model: "gemini-3.5-flash",
model: "gemini-3.7-flash",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: {},
@@ -279,7 +282,7 @@ describe("PromptQlExecutor — auth / validation", () => {
it("returns 400 when no user message is present", async () => {
const executor = new mod.PromptQlExecutor();
const result = await executor.execute({
model: "gemini-3.5-flash",
model: "gemini-3.7-flash",
body: { messages: [{ role: "assistant", content: "hi" }] },
stream: false,
credentials: { apiKey: sampleJwt },
@@ -614,7 +617,7 @@ describe("PromptQlExecutor — mocked GraphQL turn", () => {
try {
const executor = new mod.PromptQlExecutor();
const result = await executor.execute({
model: "gemini-3.5-flash",
model: "gemini-3.7-flash",
body: { messages: [{ role: "user", content: "ping" }] },
stream: false,
credentials: { apiKey: sampleJwt },
@@ -628,7 +631,7 @@ describe("PromptQlExecutor — mocked GraphQL turn", () => {
};
assert.equal(json.choices[0]!.message.content, "HELLO-PQL");
assert.equal(json.promptql_thread_id, "thread-1");
assert.equal(json.model, "gemini-3.5-flash");
assert.equal(json.model, "gemini-3.7-flash");
assert.ok(call >= 2);
assert.equal(result.response.headers.get("X-PromptQL-Thread-Id"), "thread-1");
} finally {

View File

@@ -1,76 +0,0 @@
// Regression test for #10286: gemini-3.5-flash was incorrectly marked
// supportsThinking:false, causing a spurious pre-provider HTTP 400 for any
// request with reasoning_effort set, even though the base Google AI Studio
// model supports reasoning (it has an effort-tier alias gemini-3.5-flash-high).
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-repro-10286-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "test-repro-10286-secret";
const caps = await import("../../src/lib/modelCapabilities.ts");
const core = await import("../../src/lib/db/core.ts");
const rulesDb = await import("../../src/lib/db/reasoningRoutingRules.ts");
const policy = await import("../../src/lib/reasoningRouting/policy.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
rulesDb.invalidateReasoningRoutingRuleCache();
}
function ruleInput(patch: Record<string, unknown> = {}) {
return {
name: "Enable thinking on gemini-3.5-flash",
description: "",
scope: "global",
apiKeyId: null,
comboId: null,
connectionId: null,
modelPattern: "gemini-3.5-flash",
sourceEffort: "any",
requestTags: [],
tagMatchMode: "any",
effortMode: "inherit",
targetEffort: null,
targetKind: "keep",
targetModel: null,
targetComboId: null,
budgetAction: "preserve",
budgetTokens: null,
priority: 0,
enabled: true,
...patch,
};
}
test.beforeEach(resetStorage);
test.after(async () => {
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("gemini-3.5-flash (AI Studio provider) resolves as thinking-capable", () => {
const resolved = caps.getResolvedModelCapabilities({
provider: "gemini",
model: "gemini-3.5-flash",
});
assert.equal(resolved.supportsThinking, true);
});
test("reasoning_effort 'high' on gemini-3.5-flash is NOT rejected by routing policy", async () => {
await rulesDb.createReasoningRoutingRule(ruleInput());
const decision = await policy.resolveReasoningRoutingRule({
sourceModel: "gemini/gemini-3.5-flash",
sourceModelAliases: ["gemini-3.5-flash"],
sourceEffort: "high",
hasReasoningSignal: true,
});
assert.ok(decision, "a matching rule must produce a decision");
assert.equal(decision.capability, "supported");
});

View File

@@ -71,7 +71,7 @@ test("OpenAI -> Gemini request strips encrypted from Codex collaboration tool pa
],
};
const result = openaiToGeminiRequest("gemini-3.5-flash-low", body, false) as {
const result = openaiToGeminiRequest("gemini-3.7-flash-low", body, false) as {
tools?: Array<{ functionDeclarations?: Array<{ parameters: unknown }> }>;
};

View File

@@ -14,6 +14,16 @@ const SAMPLE = {
supportedGenerationMethods: ["generateContent", "countTokens", "batchGenerateContent"],
thinking: true,
},
{
name: "models/gemini-3.5-flash",
displayName: "Gemini 3.5 Flash",
supportedGenerationMethods: ["generateContent"],
},
{
name: "models/gemini-3.5-flash-lite",
displayName: "Gemini 3.5 Flash Lite",
supportedGenerationMethods: ["generateContent"],
},
{
name: "models/gemini-3-pro-image-preview",
displayName: "Gemini 3 Pro Image Preview",
@@ -48,6 +58,12 @@ test("parseGeminiModelsList strips the models/ prefix and maps display name", ()
assert.deepEqual(flash!.supportedEndpoints, ["chat"]);
});
test("parseGeminiModelsList excludes retired Gemini 3.5 Flash but keeps Flash Lite", () => {
const ids = parseGeminiModelsList(SAMPLE).map((model) => model.id);
assert.equal(ids.includes("gemini-3.5-flash"), false);
assert.equal(ids.includes("gemini-3.5-flash-lite"), true);
});
test("parseGeminiModelsList maps generateContent image models to the chat endpoint", () => {
const models = parseGeminiModelsList(SAMPLE);
const proImage = models.find((m) => m.id === "gemini-3-pro-image-preview");

View File

@@ -56,7 +56,7 @@ test("OpenAI -> Gemini request strips strict from OpenAI-style function tool par
],
};
const result = openaiToGeminiRequest("gemini-3.5-flash-low", body, false) as {
const result = openaiToGeminiRequest("gemini-3.7-flash-low", body, false) as {
tools?: Array<{ functionDeclarations?: Array<{ parameters: unknown }> }>;
};

View File

@@ -99,7 +99,7 @@ test("buildUrl uses chat/completions endpoint for gemini models", () => {
};
// Gemini has no native shim on Copilot — it stays on /chat/completions.
assert.strictEqual(
executor.buildUrl("gemini-3.5-flash", true, 0, credentials),
executor.buildUrl("gemini-3.7-flash", true, 0, credentials),
"https://ghe.company.com/chat/completions"
);
});

View File

@@ -279,6 +279,7 @@ test("antigravity sync dynamically builds and saves mitmAlias mappings", async (
mode: "sync",
fetchedModels: [
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash" },
{ id: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash High" },
{ id: "custom-antigravity-model", name: "Custom Antigravity Model" },
],
});
@@ -289,8 +290,13 @@ test("antigravity sync dynamically builds and saves mitmAlias mappings", async (
const mitmMappings = await modelsDb.getMitmAlias("antigravity");
console.log("MITM MAPPINGS IN TEST:", mitmMappings);
// Should contain standard mapping
assert.equal(mitmMappings["gemini-3.5-flash"], "antigravity/gemini-3.5-flash");
// Retired models reported by upstream must not be imported or mapped.
assert.equal(mitmMappings["gemini-3.5-flash"], undefined);
assert.equal(
models.some((model) => model.id === "gemini-3.5-flash"),
false
);
assert.equal(mitmMappings["gemini-3.7-flash-high"], "antigravity/gemini-3.7-flash-high");
assert.equal(mitmMappings["custom-antigravity-model"], "antigravity/custom-antigravity-model");
// Removed Antigravity 2.0 preview/agent aliases must not be reintroduced.

View File

@@ -154,21 +154,14 @@ test("unknown models keep maxOutputTokens null instead of using a generic defaul
);
});
test("provider-neutral Gemini 3.5 tier IDs retain their non-thinking capabilities", () => {
test("retired Gemini 3.5 Flash IDs have no provider-neutral model specs", () => {
for (const modelId of [
"gemini-3.5-flash",
"gemini-3.5-flash-extra-low",
"gemini-3.5-flash-low",
"gemini-3-flash-agent",
]) {
const spec = MODEL_SPECS[modelId];
assert.ok(spec, `missing exact MODEL_SPECS entry for ${modelId}`);
const capabilities = modelCapabilities.getResolvedModelCapabilities(modelId);
assert.equal(capabilities.contextWindow, 1048576, modelId);
assert.equal(capabilities.maxOutputTokens, 65536, modelId);
// These ids encode the upstream reasoning tier and do not accept a client-supplied effort.
assert.equal(capabilities.supportsThinking, false, modelId);
assert.equal(capabilities.supportsTools, true, modelId);
assert.equal(capabilities.supportsVision, true, modelId);
assert.equal(MODEL_SPECS[modelId], undefined, modelId);
}
});

View File

@@ -702,6 +702,7 @@ test("v1 models catalog exposes current Antigravity aliases without retired mode
assert.equal(ids.has("antigravity/gemini-3.6-flash-high"), false);
assert.equal(ids.has("antigravity/gemini-3.6-flash-medium"), false);
assert.equal(ids.has("antigravity/gemini-3.6-flash-low"), false);
assert.equal(ids.has("antigravity/gemini-3.5-flash"), false);
assert.equal(ids.has("antigravity/gemini-3.5-flash-extra-low"), false);
assert.equal(ids.has("antigravity/gemini-3.5-flash-low"), false);
assert.equal(ids.has("antigravity/gemini-3-flash-agent"), false);

View File

@@ -60,7 +60,7 @@ test("buildOmniRouteResponseMetaHeaders keeps ASCII model header values unchange
});
test("buildOmniRouteResponseMetaHeaders percent-encodes non-ASCII model header values", () => {
const model = "free-mix/[假流式]gemini-3.5-flash";
const model = "free-mix/[假流式]gemini-3.7-flash";
const headers = buildOmniRouteResponseMetaHeaders({
provider: "openai",
model,

View File

@@ -288,7 +288,7 @@ test("hidden provider models are filtered from per-model quota rows", () => {
});
const hidden = providerLimitUtils.collectHiddenQuotaModelIds("antigravity", {
models: [{ id: "antigravity/gpt-oss-120b-medium", isHidden: true }],
modelCompatOverrides: [{ id: "gemini-3.5-flash", isHidden: true }],
modelCompatOverrides: [{ id: "gemini-3.7-flash", isHidden: true }],
});
const visible = providerLimitUtils.filterHiddenModelQuotas("antigravity", quotas, hidden);

View File

@@ -394,7 +394,7 @@ test("parseSSEToGeminiResponse extracts tool calls from textual format", () => {
})}`,
].join("\n");
const parsed = parseSSEToGeminiResponse(rawSSE, "gemini-3.5-flash-low");
const parsed = parseSSEToGeminiResponse(rawSSE, "gemini-3.7-flash-low");
assert.ok(parsed);
assert.equal(parsed.choices[0].finish_reason, "tool_calls");

View File

@@ -232,14 +232,14 @@ test("createSSEStream passthrough converts textual tool-call content into struct
id: "chatcmpl_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: { role: "assistant", content: toolText } }],
})}\n\n`,
`data: ${JSON.stringify({
id: "chatcmpl_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
})}\n\n`,
],
@@ -247,7 +247,7 @@ test("createSSEStream passthrough converts textual tool-call content into struct
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "antigravity",
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
body: {
messages: [{ role: "user", content: "inspect db" }],
},
@@ -284,21 +284,21 @@ test("createSSEStream passthrough converts split textual tool-call content at co
id: "chatcmpl_split_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: { role: "assistant", content: chunks[0] } }],
})}\n\n`,
`data: ${JSON.stringify({
id: "chatcmpl_split_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: { content: chunks[1] } }],
})}\n\n`,
`data: ${JSON.stringify({
id: "chatcmpl_split_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
})}\n\n`,
],
@@ -306,7 +306,7 @@ test("createSSEStream passthrough converts split textual tool-call content at co
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "antigravity",
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
body: { messages: [{ role: "user", content: "inspect db" }] },
onComplete(payload) {
onCompletePayload = payload;
@@ -340,28 +340,28 @@ test("createSSEStream passthrough handles textual tool-call content split inside
id: "chatcmpl_split_prefix_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: { role: "assistant", content: chunks[0] } }],
})}\n\n`,
`data: ${JSON.stringify({
id: "chatcmpl_split_prefix_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: { content: chunks[1] } }],
})}\n\n`,
`data: ${JSON.stringify({
id: "chatcmpl_split_prefix_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: { content: chunks[2] } }],
})}\n\n`,
`data: ${JSON.stringify({
id: "chatcmpl_split_prefix_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
})}\n\n`,
],
@@ -369,7 +369,7 @@ test("createSSEStream passthrough handles textual tool-call content split inside
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "antigravity",
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
body: { messages: [{ role: "user", content: "inspect db" }] },
onComplete(payload) {
onCompletePayload = payload;
@@ -515,14 +515,14 @@ Arguments: {"path":"/opt/OmniRoute/src","target":"files"}`;
id: "chatcmpl_unknown_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: { role: "assistant", content: toolText } }],
})}\n\n`,
`data: ${JSON.stringify({
id: "chatcmpl_unknown_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
})}\n\n`,
],
@@ -530,7 +530,7 @@ Arguments: {"path":"/opt/OmniRoute/src","target":"files"}`;
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "antigravity",
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
body: {
messages: [{ role: "user", content: "inspect files" }],
tools: [
@@ -561,14 +561,14 @@ test("createSSEStream passthrough suppresses malformed textual tool-call content
id: "chatcmpl_malformed_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: { role: "assistant", content: malformedToolText } }],
})}\n\n`,
`data: ${JSON.stringify({
id: "chatcmpl_malformed_textual_tool",
object: "chat.completion.chunk",
created: 1,
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
})}\n\n`,
],
@@ -576,7 +576,7 @@ test("createSSEStream passthrough suppresses malformed textual tool-call content
mode: "passthrough",
sourceFormat: FORMATS.OPENAI,
provider: "antigravity",
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
body: { messages: [{ role: "user", content: "inspect db" }] },
onComplete(payload) {
onCompletePayload = payload;
@@ -617,7 +617,7 @@ test("createSSEStream suppresses malformed compact textual tool-call content", a
targetFormat: FORMATS.ANTIGRAVITY,
sourceFormat: FORMATS.OPENAI,
provider: "antigravity",
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
body: { messages: [{ role: "user", content: "inspect files" }] },
onComplete(payload) {
onCompletePayload = payload;
@@ -1024,7 +1024,7 @@ Arguments: {"command":"systemctl status omniroute"}`;
response: {
id: "resp_textual_tool",
object: "response",
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
status: "completed",
output: [],
usage: { input_tokens: 10, output_tokens: 4, total_tokens: 14 },
@@ -1038,7 +1038,7 @@ Arguments: {"command":"systemctl status omniroute"}`;
sourceFormat: FORMATS.OPENAI_RESPONSES,
clientResponseFormat: FORMATS.OPENAI_RESPONSES,
provider: "antigravity",
model: "antigravity/gemini-3.5-flash-low",
model: "antigravity/gemini-3.7-flash-low",
body: {
input: "check service",
tools: [{ type: "function", name: "terminal", parameters: { type: "object" } }],

View File

@@ -30,6 +30,7 @@ test("T28: antigravity static catalog exposes only callable Gemini tier IDs", ()
assert.ok(!staticIds.includes("gemini-3.6-flash-high"));
assert.ok(!staticIds.includes("gemini-3.6-flash-medium"));
assert.ok(!staticIds.includes("gemini-3.6-flash-low"));
assert.ok(!staticIds.includes("gemini-3.5-flash"));
assert.ok(!staticIds.includes("gemini-3.5-flash-extra-low"));
assert.ok(!staticIds.includes("gemini-3.5-flash-low"));
assert.ok(!staticIds.includes("gemini-3-flash-agent"));

View File

@@ -607,7 +607,7 @@ test("OpenAI -> Antigravity wraps Gemini requests in a Cloud Code envelope", ()
test("OpenAI -> Antigravity Gemini omits signature-less historical tool calls and keeps response context", () => {
const result = openaiToAntigravityRequest(
"gemini-3.5-flash-low",
"gemini-3.7-flash-low",
{
messages: [
{ role: "user", content: "Update todo" },
@@ -686,7 +686,7 @@ test("OpenAI -> Antigravity Gemini omits signature-less historical tool calls an
test("OpenAI -> Antigravity preserves multiple signature-less historical tool responses as context", () => {
const result = openaiToAntigravityRequest(
"gemini-3.5-flash-low",
"gemini-3.7-flash-low",
{
messages: [
{ role: "user", content: "Inspect OmniRoute config" },
@@ -747,7 +747,7 @@ test("OpenAI -> Antigravity preserves signed Gemini tool calls in native form",
storeGeminiThoughtSignature(buildGeminiThoughtSignatureKey(ns, toolId), "SIG_AG_SIGNED_XYZ");
const result = openaiToAntigravityRequest(
"gemini-3.5-flash-low",
"gemini-3.7-flash-low",
{
messages: [
{ role: "user", content: "Read status" },
@@ -787,7 +787,7 @@ test("OpenAI -> Antigravity preserves signed Gemini tool calls in native form",
test("OpenAI -> Antigravity escapes signature-less tool response context content", () => {
const result = openaiToAntigravityRequest(
"gemini-3.5-flash-low",
"gemini-3.7-flash-low",
{
messages: [
{ role: "user", content: "Inspect previous output" },

View File

@@ -346,7 +346,7 @@ test("Gemini stream: converts textual Tool call block to structured tool_calls",
const result = geminiToOpenAIResponse(
{
responseId: "resp-textual-tool",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -386,7 +386,7 @@ test("Gemini stream: routes textual reasoning tags to reasoning_content before t
const result = geminiToOpenAIResponse(
{
responseId: "resp-textual-thought-tool",
modelVersion: "gemini-3.5-flash-high",
modelVersion: "gemini-3.7-flash-high",
candidates: [
{
content: {
@@ -431,7 +431,7 @@ test("Gemini stream: keeps textual reasoning hidden across split chunks", () =>
const first = geminiToOpenAIResponse(
{
responseId: "resp-split-thought",
modelVersion: "gemini-3.5-flash-high",
modelVersion: "gemini-3.7-flash-high",
candidates: [{ content: { parts: [{ text: "§54§ <tho" }] } }],
},
state
@@ -444,7 +444,7 @@ test("Gemini stream: keeps textual reasoning hidden across split chunks", () =>
const second = geminiToOpenAIResponse(
{
responseId: "resp-split-thought",
modelVersion: "gemini-3.5-flash-high",
modelVersion: "gemini-3.7-flash-high",
candidates: [{ content: { parts: [{ text: "ught\nNeed to inspect" }] } }],
},
state
@@ -459,7 +459,7 @@ test("Gemini stream: keeps textual reasoning hidden across split chunks", () =>
const third = geminiToOpenAIResponse(
{
responseId: "resp-split-thought",
modelVersion: "gemini-3.5-flash-high",
modelVersion: "gemini-3.7-flash-high",
candidates: [{ content: { parts: [{ text: " more</tho" }] } }],
},
state
@@ -472,7 +472,7 @@ test("Gemini stream: keeps textual reasoning hidden across split chunks", () =>
const fourth = geminiToOpenAIResponse(
{
responseId: "resp-split-thought",
modelVersion: "gemini-3.5-flash-high",
modelVersion: "gemini-3.7-flash-high",
candidates: [{ content: { parts: [{ text: "ught>Visible answer" }] } }],
},
state
@@ -498,7 +498,7 @@ test("Gemini stream: converts prefixed textual Tool call block with zero-width c
const result = geminiToOpenAIResponse(
{
responseId: "resp-textual-tool-prefixed",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -612,7 +612,7 @@ test("Gemini stream: unwraps native functionCall args when emitted as JSON strin
const result = geminiToOpenAIResponse(
{
responseId: "resp-native-tool-json-string",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -648,7 +648,7 @@ test("Gemini stream: converts JSON-string encoded textual Tool call arguments",
const result = geminiToOpenAIResponse(
{
responseId: "resp-textual-tool-json-string",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -685,7 +685,7 @@ test("Gemini stream: suppresses malformed textual Tool call marker", () => {
const result = geminiToOpenAIResponse(
{
responseId: "resp-textual-tool-malformed",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -717,7 +717,7 @@ test("Gemini stream: handles textual Tool call block split across chunks", () =>
const state = createStreamingState();
const chunk1 = {
responseId: "resp-split",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -732,7 +732,7 @@ test("Gemini stream: handles textual Tool call block split across chunks", () =>
};
const chunk2 = {
responseId: "resp-split",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -768,7 +768,7 @@ test("Gemini stream: does not swallow false positive textual tool call in backti
const state = createStreamingState();
const chunk1 = {
responseId: "resp-false-positive",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -783,7 +783,7 @@ test("Gemini stream: does not swallow false positive textual tool call in backti
};
const chunk2 = {
responseId: "resp-false-positive",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -817,7 +817,7 @@ test("Gemini stream: does not swallow terminated trailing false positive textual
const state = createStreamingState();
const chunk1 = {
responseId: "resp-false-positive-terminated",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -842,7 +842,7 @@ test("Gemini stream: flushes left part before textual tool call candidate and fl
const state = createStreamingState() as any;
const chunk1 = {
responseId: "resp-test-flush-left",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -884,7 +884,7 @@ test("Gemini stream: splits mid-stream partial candidate but preserves tool call
const state = createStreamingState() as any;
const chunk1 = {
responseId: "resp-test-split-candidate",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -932,7 +932,7 @@ test("Gemini stream: index mismatch regression test with zero-width characters i
const result = geminiToOpenAIResponse(
{
responseId: "resp-textual-tool-index-mismatch",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -964,7 +964,7 @@ test("Gemini stream: partial tool call with (empty) prefix check at chunk end do
const state = createStreamingState();
const chunk1 = {
responseId: "resp-empty-leak",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -1011,7 +1011,7 @@ test("Gemini stream: parses textual tool call that starts in a subsequent chunk
const state = createStreamingState() as any;
const chunk1 = {
responseId: "resp-test-after-prose",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -1060,7 +1060,7 @@ test("Gemini stream: checks lastParen before lastBracket when identifying partia
// Имитируем чанк, который кончается на частичный "(empty)[Tool call:" маркер, например "(em"
const chunk1 = {
responseId: "resp-test-empty-partial",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{
content: {
@@ -1187,7 +1187,7 @@ test("Gemini stream: partial textual tool call survives a reasoning-only chunk",
geminiToOpenAIResponse(
{
responseId: "resp-interleave",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [
{ content: { parts: [{ text: '[Tool call: terminal]\nArguments: {"command":"ls' }] } },
],
@@ -1200,7 +1200,7 @@ test("Gemini stream: partial textual tool call survives a reasoning-only chunk",
geminiToOpenAIResponse(
{
responseId: "resp-interleave",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [{ content: { parts: [{ text: "<thinking>pondering</thinking>" }] } }],
},
state
@@ -1221,7 +1221,7 @@ test("Gemini stream: partial textual tool call survives a reasoning-only chunk",
geminiToOpenAIResponse(
{
responseId: "resp-interleave",
modelVersion: "gemini-3.5-flash-low",
modelVersion: "gemini-3.7-flash-low",
candidates: [{ content: { parts: [{ text: '"}' }] }, finishReason: "STOP" }],
},
state