支持 Gemini Interactions 与 Antigravity 配额精细化

This commit is contained in:
MMEXA
2026-07-08 22:34:29 +08:00
parent 1ca83ca3fb
commit 80a6579766
41 changed files with 1072 additions and 225 deletions
@@ -34,9 +34,10 @@
<DropdownMenuItem
v-for="item in items"
:key="item.model"
:title="item.model"
@select="handleTestModel(item.model)"
>
{{ item.model }}
<span class="truncate">{{ item.label }}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -52,12 +53,21 @@
:key="item.model"
>
<div class="flex items-center justify-between text-[10px] mb-0.5">
<span
class="text-muted-foreground truncate mr-2 min-w-0 flex-1"
:title="item.model"
>
{{ item.label }}
</span>
<div class="min-w-0 flex-1 mr-2">
<div
class="text-muted-foreground truncate"
:title="item.model"
>
{{ item.label }}
</div>
<div
v-if="item.label !== item.model"
class="text-[9px] leading-tight text-muted-foreground/55 truncate font-mono"
:title="item.model"
>
{{ item.model }}
</div>
</div>
<span :class="getQuotaRemainingClass(item.usedPercent)">
{{ item.remainingPercent.toFixed(1) }}%
</span>
@@ -165,6 +175,51 @@ function getQuotaWindowLiveResetSeconds(
return null
}
function coercePercent(value: unknown): number | null {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) return null
return Math.min(Math.max(numericValue, 0), 100)
}
function coerceRemainingFraction(value: unknown): number | null {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) return null
return Math.min(Math.max(numericValue, 0), 1)
}
function secondsUntilUnixReset(resetAt: unknown): number | null {
const numericResetAt = Number(resetAt)
if (!Number.isFinite(numericResetAt) || numericResetAt <= 0) return null
const now = Math.floor(Date.now() / 1000)
return Math.max(Math.floor(numericResetAt - now), 0)
}
function sortQuotaItems(a: QuotaItem, b: QuotaItem): number {
return (a.remainingPercent - b.remainingPercent)
|| ((a.resetSeconds ?? Number.POSITIVE_INFINITY) - (b.resetSeconds ?? Number.POSITIVE_INFINITY))
|| a.label.localeCompare(b.label)
|| a.model.localeCompare(b.model)
}
function isOpaqueAntigravityQuotaIdentifier(value: string): boolean {
return value.trim().startsWith('RateLimitResetCredit_')
}
function resolveQuotaLabel(
model: string,
rawLabel: unknown,
opaqueDisplayIndex: { value: number },
): string {
const candidate = String(rawLabel || '').trim()
if (candidate && !isOpaqueAntigravityQuotaIdentifier(candidate)) return candidate
if (isOpaqueAntigravityQuotaIdentifier(model) || (candidate && isOpaqueAntigravityQuotaIdentifier(candidate))) {
const label = `Key-${opaqueDisplayIndex.value}`
opaqueDisplayIndex.value += 1
return label
}
return candidate || model
}
function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefined): QuotaItem[] {
if (!quota) return []
@@ -175,6 +230,7 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
? quota.windows.filter(window => String(window?.scope || '').trim().toLowerCase() === 'model')
: []
if (windows.length === 0) return []
const opaqueDisplayIndex = { value: 1 }
const items = windows
.map((window) => {
@@ -196,7 +252,7 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
return {
model,
label: String(window.label || window.model || model),
label: resolveQuotaLabel(model, window.label || window.model, opaqueDisplayIndex),
usedPercent,
remainingPercent,
resetSeconds: getQuotaWindowLiveResetSeconds(quota, window),
@@ -204,7 +260,7 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
})
.filter((item): item is QuotaItem => item !== null)
items.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model))
items.sort(sortQuotaItems)
return items
}
@@ -218,26 +274,26 @@ const items = computed<QuotaItem[]>(() => {
if (!quotaByModel || typeof quotaByModel !== 'object') return []
const result: QuotaItem[] = []
const opaqueDisplayIndex = { value: 1 }
for (const [model, rawInfo] of Object.entries(quotaByModel)) {
if (!model) continue
const info = (rawInfo || {}) as Record<string, unknown>
let usedPercent = Number(info['used_percent'])
if (!Number.isFinite(usedPercent)) {
const remainingFraction = Number(info['remaining_fraction'])
if (Number.isFinite(remainingFraction)) {
let usedPercent = coercePercent(info['used_percent'])
if (usedPercent === null) {
const remainingFraction = coerceRemainingFraction(info['remaining_fraction'])
if (remainingFraction !== null) {
usedPercent = (1 - remainingFraction) * 100
} else {
continue
}
}
if (usedPercent < 0) usedPercent = 0
if (usedPercent > 100) usedPercent = 100
usedPercent = coercePercent(usedPercent) ?? 0
const remainingPercent = Math.max(100 - usedPercent, 0)
let resetSeconds: number | null = null
let resetSeconds = secondsUntilUnixReset(info['reset_at'])
const resetTime = info['reset_time']
if (typeof resetTime === 'string' && resetTime.trim()) {
const ts = Date.parse(resetTime.trim())
@@ -247,10 +303,16 @@ const items = computed<QuotaItem[]>(() => {
}
}
result.push({ model, label: model, usedPercent, remainingPercent, resetSeconds })
result.push({
model,
label: resolveQuotaLabel(model, info['display_name'], opaqueDisplayIndex),
usedPercent,
remainingPercent,
resetSeconds,
})
}
result.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model))
result.sort(sortQuotaItems)
return result
})
@@ -361,31 +361,49 @@
/>
<div class="grid grid-cols-2 gap-3">
<ProviderQuotaProgressRow
v-for="group in getAntigravityQuotaSummaryForKey(key)"
:key="group.key"
:label="group.label"
:used-percent="group.usedPercent"
:remaining-percent="group.remainingPercent"
:meter-class="getQuotaRemainingClass(group.usedPercent)"
:bar-class="getQuotaRemainingBarColor(group.usedPercent)"
v-for="item in getAntigravityQuotaPreviewForKey(key)"
:key="item.model"
:label="item.label"
:title="item.model"
:used-percent="item.usedPercent"
:remaining-percent="item.remainingPercent"
:meter-class="getQuotaRemainingClass(item.usedPercent)"
:bar-class="getQuotaRemainingBarColor(item.usedPercent)"
>
<template #footer>
<div
v-if="group.resetSeconds !== null || group.usedPercent > 0"
class="text-[9px] text-muted-foreground/70 mt-0.5"
>
<template v-if="group.resetSeconds !== null && group.resetSeconds > 0">
{{ formatResetTime(group.resetSeconds) }}{{ legacyT('后重置') }}
</template>
<template v-else-if="group.resetSeconds !== null && group.resetSeconds <= 0">
{{ legacyT('已重置') }}
</template>
<template v-else>
{{ legacyT('重置时间未知') }}
</template>
<div class="mt-0.5 space-y-0.5">
<div
v-if="item.label !== item.model"
class="text-[9px] text-muted-foreground/55 truncate font-mono"
:title="item.model"
>
{{ item.model }}
</div>
<div
v-if="item.resetSeconds !== null || item.usedPercent > 0"
class="text-[9px] text-muted-foreground/70"
>
<template v-if="item.resetSeconds !== null && item.resetSeconds > 0">
{{ formatResetTime(item.resetSeconds) }}{{ legacyT('后重置') }}
</template>
<template v-else-if="item.resetSeconds !== null && item.resetSeconds <= 0">
{{ legacyT('已重置') }}
</template>
<template v-else>
{{ legacyT('重置时间未知') }}
</template>
</div>
</div>
</template>
</ProviderQuotaProgressRow>
<button
v-if="getAntigravityQuotaHiddenCountForKey(key) > 0"
type="button"
class="col-span-2 text-left text-[10px] text-muted-foreground hover:text-foreground transition-colors"
@click="openAntigravityQuotaDialog(key)"
>
{{ legacyT('另有') }} {{ getAntigravityQuotaHiddenCountForKey(key) }} {{ legacyT('个模型,查看全部') }}
</button>
</div>
</template>
</div>
@@ -3348,46 +3366,90 @@ function secondsUntilReset(resetTime: string): number | null {
return diff > 0 ? diff : 0
}
function secondsUntilUnixReset(resetAt: number | string | null | undefined): number | null {
const numericResetAt = Number(resetAt)
if (!Number.isFinite(numericResetAt) || numericResetAt <= 0) return null
const now = Math.floor(Date.now() / 1000)
return Math.max(Math.floor(numericResetAt - now), 0)
}
function coerceAntigravityPercent(value: number | string | null | undefined): number | undefined {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) return undefined
return Math.min(Math.max(numericValue, 0), 100)
}
function coerceAntigravityRemainingFraction(value: number | string | null | undefined): number | undefined {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) return undefined
return Math.min(Math.max(numericValue, 0), 1)
}
function antigravityQuotaItemSort(a: AntigravityQuotaItem, b: AntigravityQuotaItem): number {
return (a.remainingPercent - b.remainingPercent)
|| ((a.resetSeconds ?? Number.POSITIVE_INFINITY) - (b.resetSeconds ?? Number.POSITIVE_INFINITY))
|| a.label.localeCompare(b.label)
|| a.model.localeCompare(b.model)
}
function isOpaqueAntigravityQuotaIdentifier(value: string): boolean {
return value.trim().startsWith('RateLimitResetCredit_')
}
function resolveAntigravityQuotaLabel(
model: string,
rawLabel: unknown,
opaqueDisplayIndex: { value: number },
): string {
const candidate = String(rawLabel || '').trim()
if (candidate && !isOpaqueAntigravityQuotaIdentifier(candidate)) return candidate
if (isOpaqueAntigravityQuotaIdentifier(model) || (candidate && isOpaqueAntigravityQuotaIdentifier(candidate))) {
const label = `Key-${opaqueDisplayIndex.value}`
opaqueDisplayIndex.value += 1
return label
}
return candidate || model
}
function getAntigravityQuotaItems(metadata: UpstreamMetadata | null | undefined): AntigravityQuotaItem[] {
const quotaByModel = metadata?.antigravity?.quota_by_model
if (!quotaByModel || typeof quotaByModel !== 'object') return []
const items: AntigravityQuotaItem[] = []
const opaqueDisplayIndex = { value: 1 }
for (const [model, rawInfo] of Object.entries(quotaByModel)) {
if (!model) continue
const info: Partial<AntigravityModelQuota> = rawInfo || {}
let usedPercent = Number(info.used_percent)
if (!Number.isFinite(usedPercent)) {
const remainingFraction = Number(info.remaining_fraction)
if (Number.isFinite(remainingFraction)) {
let usedPercent = coerceAntigravityPercent(info.used_percent)
if (usedPercent === undefined) {
const remainingFraction = coerceAntigravityRemainingFraction(info.remaining_fraction)
if (remainingFraction !== undefined) {
usedPercent = (1 - remainingFraction) * 100
} else {
continue
}
}
if (usedPercent < 0) usedPercent = 0
if (usedPercent > 100) usedPercent = 100
usedPercent = coerceAntigravityPercent(usedPercent) ?? 0
const remainingPercent = Math.max(100 - usedPercent, 0)
let resetSeconds: number | null = null
let resetSeconds = secondsUntilUnixReset(info.reset_at)
if (typeof info.reset_time === 'string' && info.reset_time.trim()) {
resetSeconds = secondsUntilReset(info.reset_time.trim())
resetSeconds = secondsUntilReset(info.reset_time.trim()) ?? resetSeconds
}
items.push({
model,
label: model,
label: resolveAntigravityQuotaLabel(model, info.display_name, opaqueDisplayIndex),
usedPercent,
remainingPercent,
resetSeconds,
})
}
// 按“最紧张”(已用最多)优先排序,便于快速定位额度风险;完整列表通过滚动展示
items.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model))
items.sort(antigravityQuotaItemSort)
return items
}
@@ -3395,6 +3457,7 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
const quota = getQuotaSnapshotForProvider(key, 'antigravity')
const windows = getQuotaWindowByScope(quota, 'model')
if (!quota || windows.length === 0) return []
const opaqueDisplayIndex = { value: 1 }
const items = windows
.map((window) => {
@@ -3418,7 +3481,11 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
return {
model,
label: String(window.label || window.model || model),
label: resolveAntigravityQuotaLabel(
model,
window.label || window.model,
opaqueDisplayIndex,
),
usedPercent: normalizedUsedPercent,
remainingPercent: normalizedRemainingPercent,
resetSeconds: getQuotaWindowLiveResetSeconds(quota, window),
@@ -3426,124 +3493,24 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
})
.filter((item): item is AntigravityQuotaItem => item !== null)
items.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model))
items.sort(antigravityQuotaItemSort)
return items
}
// Antigravity 配额分组定义(按匹配优先级排列,具体规则在前)
interface AntigravityQuotaGroup {
key: string
label: string
match: (model: string) => boolean
}
const ANTIGRAVITY_QUOTA_PREVIEW_LIMIT = 6
const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroup[] = [
{ key: 'claude', label: 'Claude', match: m => m.includes('claude') },
{ key: 'gemini-2.5', label: 'Gemini 2.5', match: m => m.includes('gemini-2.5') || m.includes('gemini-2-5') },
{ key: 'gemini-3', label: 'Gemini 3', match: m => m.includes('gemini-3') && !m.includes('image') },
{ key: 'gemini-3-image', label: 'Gemini 3 Image', match: m => m.includes('gemini-3') && m.includes('image') },
]
interface AntigravityQuotaSummaryItem {
key: string
label: string
usedPercent: number // 组内最高已用百分比(最紧张)
remainingPercent: number // 100 - usedPercent
resetSeconds: number | null
}
function getAntigravityQuotaSummary(metadata: UpstreamMetadata | null | undefined): AntigravityQuotaSummaryItem[] {
const items = getAntigravityQuotaItems(metadata)
if (!items.length) return []
// 将每个模型归入分组
const groupMap = new Map<string, { label: string, maxUsed: number, resetSeconds: number | null }>()
for (const item of items) {
const model = item.model.toLowerCase()
const group = ANTIGRAVITY_QUOTA_GROUPS.find(g => g.match(model))
if (!group) continue
const existing = groupMap.get(group.key)
if (!existing) {
groupMap.set(group.key, {
label: group.label,
maxUsed: item.usedPercent,
resetSeconds: item.resetSeconds,
})
} else {
if (item.usedPercent > existing.maxUsed) {
existing.maxUsed = item.usedPercent
}
if (existing.resetSeconds === null) {
existing.resetSeconds = item.resetSeconds
} else if (item.resetSeconds !== null && item.resetSeconds < existing.resetSeconds) {
existing.resetSeconds = item.resetSeconds
}
}
}
// 按 ANTIGRAVITY_QUOTA_GROUPS 定义的顺序输出
const result: AntigravityQuotaSummaryItem[] = []
for (const group of ANTIGRAVITY_QUOTA_GROUPS) {
const data = groupMap.get(group.key)
if (!data) continue
result.push({
key: group.key,
label: data.label,
usedPercent: data.maxUsed,
remainingPercent: Math.max(100 - data.maxUsed, 0),
resetSeconds: data.resetSeconds,
})
}
return result
}
function getAntigravityQuotaSummaryForKey(key: EndpointAPIKey): AntigravityQuotaSummaryItem[] {
function getAntigravityQuotaItemsForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
const snapshotItems = getAntigravityQuotaItemsFromSnapshot(key)
if (snapshotItems.length > 0) {
const groupMap = new Map<string, { label: string, maxUsed: number, resetSeconds: number | null }>()
if (snapshotItems.length > 0) return snapshotItems
return getAntigravityQuotaItems(key.upstream_metadata)
}
for (const item of snapshotItems) {
const model = item.model.toLowerCase()
const group = ANTIGRAVITY_QUOTA_GROUPS.find(g => g.match(model))
if (!group) continue
function getAntigravityQuotaPreviewForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
return getAntigravityQuotaItemsForKey(key).slice(0, ANTIGRAVITY_QUOTA_PREVIEW_LIMIT)
}
const existing = groupMap.get(group.key)
if (!existing) {
groupMap.set(group.key, {
label: group.label,
maxUsed: item.usedPercent,
resetSeconds: item.resetSeconds,
})
} else {
if (item.usedPercent > existing.maxUsed) {
existing.maxUsed = item.usedPercent
}
if (existing.resetSeconds === null) {
existing.resetSeconds = item.resetSeconds
} else if (item.resetSeconds !== null && item.resetSeconds < existing.resetSeconds) {
existing.resetSeconds = item.resetSeconds
}
}
}
const result: AntigravityQuotaSummaryItem[] = []
for (const group of ANTIGRAVITY_QUOTA_GROUPS) {
const data = groupMap.get(group.key)
if (!data) continue
result.push({
key: group.key,
label: data.label,
usedPercent: data.maxUsed,
remainingPercent: Math.max(100 - data.maxUsed, 0),
resetSeconds: data.resetSeconds,
})
}
return result
}
return getAntigravityQuotaSummary(key.upstream_metadata)
function getAntigravityQuotaHiddenCountForKey(key: EndpointAPIKey): number {
return Math.max(getAntigravityQuotaItemsForKey(key).length - ANTIGRAVITY_QUOTA_PREVIEW_LIMIT, 0)
}
function getResetCountdownText(
@@ -0,0 +1,124 @@
import { describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h } from 'vue'
import AntigravityQuotaDialog from '@/features/providers/components/AntigravityQuotaDialog.vue'
import type { UpstreamMetadata } from '@/api/endpoints/types'
vi.mock('@/components/ui', async () => {
const { defineComponent, h } = await import('vue')
const passthrough = (name: string) => defineComponent({
name,
setup(_, { slots }) {
return () => h('div', [
slots.headerActions?.(),
slots.default?.(),
slots.footer?.(),
])
},
})
return {
Dialog: passthrough('DialogStub'),
DropdownMenu: passthrough('DropdownMenuStub'),
DropdownMenuTrigger: passthrough('DropdownMenuTriggerStub'),
DropdownMenuContent: passthrough('DropdownMenuContentStub'),
DropdownMenuItem: defineComponent({
name: 'DropdownMenuItemStub',
emits: ['select'],
setup(_, { emit, slots }) {
return () => h('button', { type: 'button', onClick: () => emit('select') }, slots.default?.())
},
}),
}
})
vi.mock('@/components/ui/button.vue', async () => {
const { defineComponent, h } = await import('vue')
return {
default: defineComponent({
name: 'ButtonStub',
setup(_, { attrs, slots }) {
return () => h('button', { ...attrs, type: 'button' }, slots.default?.())
},
}),
}
})
vi.mock('lucide-vue-next', async () => {
const { defineComponent, h } = await import('vue')
const Icon = defineComponent({
name: 'IconStub',
setup() {
return () => h('span')
},
})
return {
BarChart3: Icon,
Loader2: Icon,
Play: Icon,
}
})
vi.mock('@/api/endpoints/providers', () => ({
testModel: vi.fn(),
}))
vi.mock('@/composables/useToast', () => ({
useToast: () => ({
error: vi.fn(),
success: vi.fn(),
}),
}))
vi.mock('@/utils/errorParser', () => ({
parseApiError: (value: unknown) => String(value),
}))
function mount(metadata: UpstreamMetadata) {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(defineComponent({
setup() {
return () => h(AntigravityQuotaDialog, {
open: true,
metadata,
keyName: 'Key-1',
})
},
}))
app.mount(root)
return {
root,
unmount: () => {
app.unmount()
root.remove()
},
}
}
describe('AntigravityQuotaDialog', () => {
it('renders model display names without losing raw model identifiers', () => {
const { root, unmount } = mount({
antigravity: {
quota_by_model: {
'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb': {
display_name: 'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb',
remaining_fraction: 0.25,
used_percent: 75,
},
},
},
})
expect(root.textContent).toContain('Key-1')
expect(root.textContent).toContain('RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb')
expect(root.textContent).toContain('25.0%')
unmount()
})
})
@@ -5,6 +5,7 @@ import { getDefaultEndpointBaseUrl, getDefaultEndpointPath } from '../endpoint-d
const apiFormats = [
{ value: 'openai:chat', default_path: '/v1/chat/completions' },
{ value: 'gemini:generate_content', default_path: '/v1beta/models/{model}:{action}' },
{ value: 'gemini:interactions', default_path: '/v1/interactions' },
{ value: 'gemini:embedding', default_path: '/v1beta/models/{model}:embedContent' },
{ value: 'gemini:video', default_path: '/v1beta/models/{model}:predictLongRunning' },
{ value: 'openai:responses', default_path: '/v1/responses' },
@@ -31,6 +32,12 @@ describe('endpoint default paths', () => {
apiFormats,
})).toBe('/models/{model}:embedContent')
expect(getDefaultEndpointPath({
apiFormat: 'gemini:interactions',
providerType: 'custom',
apiFormats,
})).toBe('/interactions')
expect(getDefaultEndpointPath({
apiFormat: 'gemini:video',
providerType: 'custom',
@@ -218,6 +225,11 @@ describe('endpoint default paths', () => {
baseUrl: 'https://generativelanguage.googleapis.com',
})).toBe('https://generativelanguage.googleapis.com/v1beta')
expect(getDefaultEndpointBaseUrl({
apiFormat: 'gemini:interactions',
baseUrl: 'https://generativelanguage.googleapis.com',
})).toBe('https://generativelanguage.googleapis.com/v1')
expect(getDefaultEndpointBaseUrl({
apiFormat: 'gemini:video',
baseUrl: 'https://generativelanguage.googleapis.com',
@@ -98,11 +98,17 @@ function usesVersionedApiRootByDefault(apiFormat: string): boolean {
|| apiFormat === 'jina:rerank'
|| apiFormat === 'claude:messages'
|| apiFormat === 'gemini:generate_content'
|| apiFormat === 'gemini:interactions'
|| apiFormat === 'gemini:embedding'
|| apiFormat === 'gemini:video'
}
function versionedApiRootSuffix(apiFormat: string): '/v1' | '/v1beta' {
if (
apiFormat === 'gemini:interactions'
) {
return '/v1'
}
if (
apiFormat === 'gemini:generate_content'
|| apiFormat === 'gemini:embedding'
@@ -116,6 +122,7 @@ function versionedApiRootSuffix(apiFormat: string): '/v1' | '/v1beta' {
function skipsVersionedApiRootDefault(apiFormat: string, baseUrl: string): boolean {
if (
apiFormat === 'gemini:generate_content'
|| apiFormat === 'gemini:interactions'
|| apiFormat === 'gemini:embedding'
|| apiFormat === 'gemini:video'
) {
@@ -86,6 +86,17 @@ describe('buildDefaultModelTestRequestBody', () => {
expect(body.messages).toBeUndefined()
})
it('uses Gemini Interactions payloads for interaction endpoints', () => {
const body = JSON.parse(buildDefaultModelTestRequestBody('gemini-3.5-flash', 'gemini:interactions'))
expect(body).toEqual({
model: 'gemini-3.5-flash',
input: 'Hello! This is a test message.',
stream: true,
})
expect(body.messages).toBeUndefined()
})
it('uses image generation tools for image models on OpenAI Responses endpoints', () => {
const body = JSON.parse(buildDefaultModelTestRequestBody(
'gpt-image-2',
@@ -196,6 +196,14 @@ export function buildDefaultModelTestRequestBody(
}, null, 2)
}
if (normalizedApiFormat === 'gemini:interactions') {
return JSON.stringify({
model: modelName,
input: DEFAULT_MODEL_TEST_MESSAGE,
stream: true,
}, null, 2)
}
if (normalizedApiFormat === 'openai:responses' && modelSupportsImageGeneration(model)) {
return JSON.stringify({
model: modelName,