mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: Antigravity 端点签名迁移至 gemini:chat,流式 usage 提取重构与请求详情自动刷新
- Antigravity 端点签名从 gemini:cli 统一为 gemini:chat,保留向后兼容,含 DB 迁移 - 流式处理中 usage/completion/text 提取抽离为 _update_ctx_from_provider_event, 支持 envelope 解包后提取,避免格式转换流程中重复计数 - 新增 has_format_conversion 属性,区分真正的格式转换与 envelope rewrite, 修正 usage 展示层的格式转换标记 - 请求详情抽屉对未完成请求支持自动轮询刷新,关闭时自动停止 - 按次计费样式调整;实际成本仅在倍率非 1.0 时显示;缓存日志降级为 trace
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""Update Antigravity endpoint signature to gemini:chat
|
||||
|
||||
Revision ID: e1b2c3d4f5a6
|
||||
Revises: b5c6d7e8f9a0
|
||||
Create Date: 2026-02-06 23:45:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "e1b2c3d4f5a6"
|
||||
down_revision: str | None = "b5c6d7e8f9a0"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# --- provider_endpoints ---
|
||||
# Update only when there is no conflicting gemini:chat endpoint for the same provider
|
||||
# (provider_endpoints has a unique constraint on (provider_id, api_format)).
|
||||
conn.execute(text("""
|
||||
UPDATE provider_endpoints pe
|
||||
SET
|
||||
api_format = 'gemini:chat',
|
||||
api_family = 'gemini',
|
||||
endpoint_kind = 'chat'
|
||||
WHERE pe.api_format = 'gemini:cli'
|
||||
AND pe.provider_id IN (
|
||||
SELECT id FROM providers WHERE lower(provider_type) = 'antigravity'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM provider_endpoints pe2
|
||||
WHERE pe2.provider_id = pe.provider_id
|
||||
AND pe2.api_format = 'gemini:chat'
|
||||
)
|
||||
"""))
|
||||
|
||||
# Best-effort normalization for already-existing Antigravity gemini:chat endpoints.
|
||||
conn.execute(text("""
|
||||
UPDATE provider_endpoints pe
|
||||
SET
|
||||
api_family = 'gemini',
|
||||
endpoint_kind = 'chat'
|
||||
WHERE pe.api_format = 'gemini:chat'
|
||||
AND pe.provider_id IN (
|
||||
SELECT id FROM providers WHERE lower(provider_type) = 'antigravity'
|
||||
)
|
||||
"""))
|
||||
|
||||
# --- provider_api_keys.api_formats (JSON array) ---
|
||||
# Replace "gemini:cli" with "gemini:chat" in the JSON array for Antigravity keys.
|
||||
# Uses text-level replace on the serialized JSON — safe because the value is a
|
||||
# simple string with no special characters that could cause ambiguous replacements.
|
||||
conn.execute(text("""
|
||||
UPDATE provider_api_keys pak
|
||||
SET api_formats = replace(pak.api_formats::text, '"gemini:cli"', '"gemini:chat"')::json
|
||||
WHERE pak.provider_id IN (
|
||||
SELECT id FROM providers WHERE lower(provider_type) = 'antigravity'
|
||||
)
|
||||
AND pak.api_formats IS NOT NULL
|
||||
AND pak.api_formats::text LIKE '%"gemini:cli"%'
|
||||
"""))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# --- provider_endpoints ---
|
||||
conn.execute(text("""
|
||||
UPDATE provider_endpoints pe
|
||||
SET
|
||||
api_format = 'gemini:cli',
|
||||
api_family = 'gemini',
|
||||
endpoint_kind = 'cli'
|
||||
WHERE pe.api_format = 'gemini:chat'
|
||||
AND pe.provider_id IN (
|
||||
SELECT id FROM providers WHERE lower(provider_type) = 'antigravity'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM provider_endpoints pe2
|
||||
WHERE pe2.provider_id = pe.provider_id
|
||||
AND pe2.api_format = 'gemini:cli'
|
||||
)
|
||||
"""))
|
||||
|
||||
# Best-effort normalization for already-existing Antigravity gemini:cli endpoints.
|
||||
conn.execute(text("""
|
||||
UPDATE provider_endpoints pe
|
||||
SET
|
||||
api_family = 'gemini',
|
||||
endpoint_kind = 'cli'
|
||||
WHERE pe.api_format = 'gemini:cli'
|
||||
AND pe.provider_id IN (
|
||||
SELECT id FROM providers WHERE lower(provider_type) = 'antigravity'
|
||||
)
|
||||
"""))
|
||||
|
||||
# --- provider_api_keys.api_formats (JSON array) ---
|
||||
conn.execute(text("""
|
||||
UPDATE provider_api_keys pak
|
||||
SET api_formats = replace(pak.api_formats::text, '"gemini:chat"', '"gemini:cli"')::json
|
||||
WHERE pak.provider_id IN (
|
||||
SELECT id FROM providers WHERE lower(provider_type) = 'antigravity'
|
||||
)
|
||||
AND pak.api_formats IS NOT NULL
|
||||
AND pak.api_formats::text LIKE '%"gemini:chat"%'
|
||||
"""))
|
||||
@@ -161,6 +161,7 @@ export interface RequestDetail {
|
||||
request_type: string
|
||||
is_stream: boolean
|
||||
status_code: number
|
||||
status?: string // pending, streaming, completed, failed, cancelled
|
||||
error_message?: string
|
||||
response_time_ms: number
|
||||
created_at: string
|
||||
|
||||
@@ -184,7 +184,7 @@ async function handleTestModel(modelName: string) {
|
||||
provider_id: props.providerId,
|
||||
model_name: modelName,
|
||||
api_key_id: props.keyId,
|
||||
api_format: 'gemini:cli',
|
||||
api_format: 'gemini:chat',
|
||||
message: 'hello',
|
||||
})
|
||||
|
||||
|
||||
@@ -642,19 +642,28 @@ const navigateGroup = (direction: number) => {
|
||||
}
|
||||
|
||||
// 加载请求追踪数据
|
||||
const loadTrace = async () => {
|
||||
const isSilentRefresh = ref(false)
|
||||
const loadTrace = async (silent = false) => {
|
||||
if (!props.requestId) return
|
||||
|
||||
loading.value = true
|
||||
isSilentRefresh.value = silent
|
||||
|
||||
if (!silent) {
|
||||
loading.value = true
|
||||
}
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
trace.value = await requestTraceApi.getRequestTrace(props.requestId)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || err.message || '加载失败'
|
||||
if (!silent) {
|
||||
error.value = err.response?.data?.detail || err.message || '加载失败'
|
||||
}
|
||||
log.error('加载请求追踪失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (!silent) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,6 +671,12 @@ const loadTrace = async () => {
|
||||
watch(groupedTimeline, (newGroups) => {
|
||||
if (!newGroups || newGroups.length === 0) return
|
||||
|
||||
// 静默刷新时不重置选中状态
|
||||
if (isSilentRefresh.value) {
|
||||
isSilentRefresh.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 查找成功的组
|
||||
const successIdx = newGroups.findIndex(g => g.primaryStatus === 'success')
|
||||
if (successIdx >= 0) {
|
||||
@@ -712,6 +727,8 @@ watch(() => props.requestId, () => {
|
||||
loadTrace()
|
||||
}, { immediate: true })
|
||||
|
||||
defineExpose({ refresh: () => loadTrace(true) })
|
||||
|
||||
// 格式化时间(详细)
|
||||
const formatTime = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
|
||||
@@ -66,13 +66,13 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:disabled="loading"
|
||||
title="刷新"
|
||||
:disabled="loading && !autoRefreshing"
|
||||
:title="autoRefreshing ? '停止自动刷新' : '刷新'"
|
||||
@click="refreshDetail"
|
||||
>
|
||||
<RefreshCw
|
||||
class="w-4 h-4"
|
||||
:class="{ 'animate-spin': loading }"
|
||||
:class="{ 'animate-spin': loading || autoRefreshing, 'text-primary': autoRefreshing }"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -275,25 +275,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== 3. 按次计费(独立隔离) ========== -->
|
||||
<!-- ========== 3. 按次计费 ========== -->
|
||||
<div
|
||||
v-if="perRequestCost > 0 && !detail.video_billing"
|
||||
class="rounded-lg p-3 bg-amber-500/5 border border-amber-500/30 mb-3"
|
||||
class="space-y-2 mb-3"
|
||||
>
|
||||
<div class="flex items-center justify-between text-xs mb-2">
|
||||
<span class="font-medium text-amber-600 dark:text-amber-400">按次计费</span>
|
||||
<span
|
||||
v-if="detail.price_per_request"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
单价 ${{ detail.price_per_request.toFixed(6) }}/次
|
||||
</span>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="font-medium text-foreground">按次计费</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">请求次数</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">1</span>
|
||||
<span class="text-xs font-mono font-medium">${{ perRequestCost.toFixed(6) }}</span>
|
||||
<div class="rounded-lg p-3 bg-primary/5 border border-primary/30 space-y-2">
|
||||
<div
|
||||
v-if="detail.price_per_request"
|
||||
class="flex items-center justify-end text-xs"
|
||||
>
|
||||
<span class="text-muted-foreground">${{ detail.price_per_request.toFixed(6) }}/次</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">请求次数</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">1</span>
|
||||
<span class="text-xs font-mono font-medium">${{ perRequestCost.toFixed(6) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -361,6 +363,7 @@
|
||||
<!-- 请求链路追踪卡片 -->
|
||||
<div v-if="detail.request_id || detail.id">
|
||||
<HorizontalRequestTimeline
|
||||
ref="timelineRef"
|
||||
:request-id="detail.request_id || detail.id"
|
||||
:override-status-code="detail.status_code"
|
||||
/>
|
||||
@@ -590,7 +593,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ref, watch, computed, onBeforeUnmount } from 'vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
@@ -631,6 +634,7 @@ const emit = defineEmits<{
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const detail = ref<RequestDetail | null>(null)
|
||||
const timelineRef = ref<InstanceType<typeof HorizontalRequestTimeline> | null>(null)
|
||||
const activeTab = ref('request-body')
|
||||
const copiedStates = ref<Record<string, boolean>>({})
|
||||
const viewMode = ref<'compare' | 'formatted' | 'raw'>('formatted')
|
||||
@@ -645,6 +649,8 @@ const historicalPricing = ref<{
|
||||
cache_read_price: string
|
||||
request_price: string
|
||||
} | null>(null)
|
||||
const autoRefreshTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const autoRefreshing = ref(false)
|
||||
|
||||
// 监听标签页切换
|
||||
watch(activeTab, (newTab) => {
|
||||
@@ -870,26 +876,38 @@ watch(() => props.requestId, async (newId) => {
|
||||
watch(() => props.isOpen, async (isOpen) => {
|
||||
if (isOpen && props.requestId) {
|
||||
await loadDetail(props.requestId)
|
||||
} else if (!isOpen) {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadDetail(id: string) {
|
||||
loading.value = true
|
||||
async function loadDetail(id: string, silent = false) {
|
||||
if (!silent) {
|
||||
loading.value = true
|
||||
historicalPricing.value = null
|
||||
}
|
||||
error.value = null
|
||||
historicalPricing.value = null
|
||||
try {
|
||||
detail.value = await dashboardApi.getRequestDetail(id)
|
||||
|
||||
// 默认显示有内容的第一个可见 tab
|
||||
const visibleTabNames = visibleTabs.value.map(t => t.name)
|
||||
if (detail.value.request_body && visibleTabNames.includes('request-body')) {
|
||||
activeTab.value = 'request-body'
|
||||
} else if (detail.value.response_body && visibleTabNames.includes('response-body')) {
|
||||
activeTab.value = 'response-body'
|
||||
} else if (visibleTabNames.length > 0) {
|
||||
activeTab.value = visibleTabNames[0]
|
||||
// 首次加载时选择默认 tab
|
||||
if (!silent) {
|
||||
const visibleTabNames = visibleTabs.value.map(t => t.name)
|
||||
if (detail.value.request_body && visibleTabNames.includes('request-body')) {
|
||||
activeTab.value = 'request-body'
|
||||
} else if (detail.value.response_body && visibleTabNames.includes('response-body')) {
|
||||
activeTab.value = 'response-body'
|
||||
} else if (visibleTabNames.length > 0) {
|
||||
activeTab.value = visibleTabNames[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 根据数据可用性自动选择请求头数据源
|
||||
// provider_request_headers 在 streaming 完成后才写入,pending/streaming 期间为空
|
||||
const hasProviderReqHeaders = detail.value.provider_request_headers &&
|
||||
Object.keys(detail.value.provider_request_headers).length > 0
|
||||
dataSource.value = hasProviderReqHeaders ? 'provider' : 'client'
|
||||
|
||||
// 使用请求记录中保存的历史价格
|
||||
if (detail.value.input_price_per_1m || detail.value.output_price_per_1m || detail.value.price_per_request) {
|
||||
historicalPricing.value = {
|
||||
@@ -900,24 +918,81 @@ async function loadDetail(id: string) {
|
||||
request_price: detail.value.price_per_request ? detail.value.price_per_request.toFixed(4) : 'N/A'
|
||||
}
|
||||
}
|
||||
|
||||
// 静默刷新时同步刷新链路追踪
|
||||
if (silent) {
|
||||
timelineRef.value?.refresh()
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Failed to load request detail:', err)
|
||||
error.value = '加载请求详情失败'
|
||||
if (!silent) {
|
||||
error.value = '加载请求详情失败'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (!silent) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
stopAutoRefresh()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
async function refreshDetail() {
|
||||
if (props.requestId) {
|
||||
await loadDetail(props.requestId)
|
||||
}
|
||||
function isRequestCompleted(): boolean {
|
||||
if (!detail.value?.status) return true
|
||||
return !['pending', 'streaming'].includes(detail.value.status)
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (autoRefreshTimer.value) {
|
||||
clearInterval(autoRefreshTimer.value)
|
||||
autoRefreshTimer.value = null
|
||||
}
|
||||
autoRefreshing.value = false
|
||||
}
|
||||
|
||||
async function refreshDetail() {
|
||||
if (!props.requestId) return
|
||||
|
||||
// 已完成:单次静默刷新
|
||||
if (isRequestCompleted()) {
|
||||
await loadDetail(props.requestId, true)
|
||||
return
|
||||
}
|
||||
|
||||
// 未完成:如果已在自动刷新则停止,否则启动
|
||||
if (autoRefreshing.value) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
|
||||
autoRefreshing.value = true
|
||||
await loadDetail(props.requestId, true)
|
||||
|
||||
// 加载后可能已经完成了
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
|
||||
autoRefreshTimer.value = setInterval(async () => {
|
||||
if (!props.requestId || !props.isOpen) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
await loadDetail(props.requestId, true)
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopAutoRefresh()
|
||||
})
|
||||
|
||||
function formatDateTime(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return 'N/A'
|
||||
const date = new Date(dateStr)
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
<div class="flex flex-col items-end flex-shrink-0">
|
||||
<span class="text-xs text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
|
||||
<span
|
||||
v-if="showActualCost && record.actual_cost !== undefined"
|
||||
v-if="showActualCost && record.actual_cost !== undefined && record.rate_multiplier && record.rate_multiplier !== 1.0"
|
||||
class="text-[10px] text-muted-foreground"
|
||||
>{{ formatCurrency(record.actual_cost) }}</span>
|
||||
</div>
|
||||
@@ -562,7 +562,7 @@
|
||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||
<span class="text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
|
||||
<span
|
||||
v-if="showActualCost && record.actual_cost !== undefined"
|
||||
v-if="showActualCost && record.actual_cost !== undefined && record.rate_multiplier && record.rate_multiplier !== 1.0"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ formatCurrency(record.actual_cost) }}
|
||||
|
||||
@@ -1009,7 +1009,7 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
||||
|
||||
# 获取端点:
|
||||
# - Codex: openai:cli
|
||||
# - Antigravity: gemini:cli(用于触发 oauth 刷新 + 提供 auth_config.project_id)
|
||||
# - Antigravity: gemini:chat(用于触发 oauth 刷新 + 提供 auth_config.project_id)
|
||||
endpoint = None
|
||||
if provider_type == ProviderType.CODEX:
|
||||
for ep in provider.endpoints:
|
||||
@@ -1019,12 +1019,16 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
||||
if not endpoint:
|
||||
raise InvalidRequestException("找不到有效的 openai:cli 端点")
|
||||
else:
|
||||
for ep in provider.endpoints:
|
||||
if ep.api_format == "gemini:cli" and ep.is_active:
|
||||
endpoint = ep
|
||||
# Prefer the new signature, but keep backward-compat with existing DB rows.
|
||||
for sig in ("gemini:chat", "gemini:cli"):
|
||||
for ep in provider.endpoints:
|
||||
if ep.api_format == sig and ep.is_active:
|
||||
endpoint = ep
|
||||
break
|
||||
if endpoint is not None:
|
||||
break
|
||||
if not endpoint:
|
||||
raise InvalidRequestException("找不到有效的 gemini:cli 端点")
|
||||
raise InvalidRequestException("找不到有效的 gemini:chat/gemini:cli 端点")
|
||||
|
||||
results: list[dict] = []
|
||||
success_count = 0
|
||||
|
||||
@@ -70,7 +70,7 @@ async def _resolve_key_auth(
|
||||
auth_config: dict[str, Any] | None = None
|
||||
|
||||
if auth_type == "oauth":
|
||||
endpoint_api_format = "gemini:cli" if provider_type == ProviderType.ANTIGRAVITY else None
|
||||
endpoint_api_format = "gemini:chat" if provider_type == ProviderType.ANTIGRAVITY else None
|
||||
try:
|
||||
resolved = await resolve_oauth_access_token(
|
||||
key_id=str(api_key.id),
|
||||
|
||||
@@ -1181,6 +1181,7 @@ class AdminUsageDetailAdapter(AdminApiAdapter):
|
||||
"is_stream": usage_record.is_stream,
|
||||
"status_code": usage_record.status_code,
|
||||
"error_message": usage_record.error_message,
|
||||
"status": usage_record.status,
|
||||
"response_time_ms": usage_record.response_time_ms,
|
||||
"first_byte_time_ms": usage_record.first_byte_time_ms, # 首字时间 (TTFB)
|
||||
"created_at": usage_record.created_at.isoformat() if usage_record.created_at else None,
|
||||
|
||||
@@ -571,7 +571,7 @@ class BaseMessageHandler:
|
||||
api_format = ctx.api_format
|
||||
# 格式转换追踪
|
||||
endpoint_api_format = ctx.provider_api_format or None
|
||||
has_format_conversion = ctx.needs_conversion
|
||||
has_format_conversion = ctx.has_format_conversion
|
||||
|
||||
# 如果 provider 为空,记录警告(不应该发生,但用于调试)
|
||||
if not provider:
|
||||
|
||||
@@ -40,7 +40,7 @@ from src.api.handlers.base.base_handler import (
|
||||
from src.api.handlers.base.parsers import get_parser_for_format
|
||||
from src.api.handlers.base.request_builder import PassthroughRequestBuilder, get_provider_auth
|
||||
from src.api.handlers.base.response_parser import ResponseParser
|
||||
from src.api.handlers.base.stream_context import StreamContext
|
||||
from src.api.handlers.base.stream_context import StreamContext, is_format_converted
|
||||
from src.api.handlers.base.stream_processor import StreamProcessor
|
||||
from src.api.handlers.base.stream_telemetry import StreamTelemetryRecorder
|
||||
from src.api.handlers.base.upstream_stream_bridge import (
|
||||
@@ -1322,7 +1322,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
client_response_headers=client_response_headers,
|
||||
# 格式转换追踪
|
||||
endpoint_api_format=ctx.provider_api_format or None,
|
||||
has_format_conversion=ctx.needs_conversion,
|
||||
has_format_conversion=ctx.has_format_conversion,
|
||||
target_model=ctx.mapped_model,
|
||||
)
|
||||
|
||||
@@ -1367,7 +1367,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
provider_request_body: dict[str, Any] | None = None
|
||||
provider_api_format_for_error: str | None = None
|
||||
client_api_format_for_error: str | None = None
|
||||
needs_conversion_for_error: bool = False
|
||||
needs_conversion_for_error: bool = False # 用于构建错误 payload(含 envelope rewrite)
|
||||
provider_id: str | None = None # Provider ID(用于失败记录)
|
||||
endpoint_id: str | None = None # Endpoint ID(用于失败记录)
|
||||
key_id: str | None = None # Key ID(用于失败记录)
|
||||
@@ -1795,7 +1795,9 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
api_format=api_format,
|
||||
# 格式转换追踪
|
||||
endpoint_api_format=provider_api_format_for_error or None,
|
||||
has_format_conversion=needs_conversion_for_error,
|
||||
has_format_conversion=is_format_converted(
|
||||
provider_api_format_for_error, client_api_format_for_error
|
||||
),
|
||||
provider_id=provider_id,
|
||||
provider_endpoint_id=endpoint_id,
|
||||
provider_api_key_id=key_id,
|
||||
@@ -1865,7 +1867,9 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
client_response_headers={"content-type": "application/json"},
|
||||
# 格式转换追踪
|
||||
endpoint_api_format=provider_api_format_for_error or None,
|
||||
has_format_conversion=needs_conversion_for_error,
|
||||
has_format_conversion=is_format_converted(
|
||||
provider_api_format_for_error, client_api_format_for_error
|
||||
),
|
||||
target_model=mapped_model_result,
|
||||
request_metadata=request_metadata,
|
||||
)
|
||||
@@ -1916,7 +1920,9 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
client_response_headers={"content-type": "application/json"},
|
||||
# 格式转换追踪
|
||||
endpoint_api_format=provider_api_format_for_error or None,
|
||||
has_format_conversion=needs_conversion_for_error,
|
||||
has_format_conversion=is_format_converted(
|
||||
provider_api_format_for_error, client_api_format_for_error
|
||||
),
|
||||
# 模型映射信息
|
||||
target_model=mapped_model_result,
|
||||
request_metadata=request_metadata,
|
||||
|
||||
@@ -43,7 +43,7 @@ from src.api.handlers.base.request_builder import PassthroughRequestBuilder, get
|
||||
from src.api.handlers.base.response_parser import (
|
||||
ResponseParser,
|
||||
)
|
||||
from src.api.handlers.base.stream_context import StreamContext
|
||||
from src.api.handlers.base.stream_context import StreamContext, is_format_converted
|
||||
from src.api.handlers.base.upstream_stream_bridge import (
|
||||
aggregate_upstream_stream_to_internal_response,
|
||||
)
|
||||
@@ -2459,7 +2459,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
||||
response_headers=ctx.response_headers,
|
||||
client_response_headers=client_response_headers,
|
||||
endpoint_api_format=ctx.provider_api_format or None,
|
||||
has_format_conversion=ctx.needs_conversion,
|
||||
has_format_conversion=ctx.has_format_conversion,
|
||||
target_model=ctx.mapped_model,
|
||||
request_metadata=request_metadata,
|
||||
)
|
||||
@@ -2492,7 +2492,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
||||
client_response_headers=client_response_headers,
|
||||
# 格式转换追踪
|
||||
endpoint_api_format=ctx.provider_api_format or None,
|
||||
has_format_conversion=ctx.needs_conversion,
|
||||
has_format_conversion=ctx.has_format_conversion,
|
||||
# 模型映射信息
|
||||
target_model=ctx.mapped_model,
|
||||
request_metadata=request_metadata,
|
||||
@@ -2552,7 +2552,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
||||
api_format=ctx.api_format,
|
||||
# 格式转换追踪
|
||||
endpoint_api_format=ctx.provider_api_format or None,
|
||||
has_format_conversion=ctx.needs_conversion,
|
||||
has_format_conversion=ctx.has_format_conversion,
|
||||
# Provider 侧追踪信息(用于记录真实成本)
|
||||
provider_id=ctx.provider_id,
|
||||
provider_endpoint_id=ctx.endpoint_id,
|
||||
@@ -2709,7 +2709,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
||||
client_response_headers=client_response_headers,
|
||||
# 格式转换追踪
|
||||
endpoint_api_format=ctx.provider_api_format or None,
|
||||
has_format_conversion=ctx.needs_conversion,
|
||||
has_format_conversion=ctx.has_format_conversion,
|
||||
# 模型映射信息
|
||||
target_model=ctx.mapped_model,
|
||||
request_metadata=request_metadata,
|
||||
@@ -3170,7 +3170,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
||||
api_format=api_format,
|
||||
# 格式转换追踪
|
||||
endpoint_api_format=provider_api_format or None,
|
||||
has_format_conversion=needs_conversion,
|
||||
has_format_conversion=is_format_converted(provider_api_format, str(api_format)),
|
||||
# Provider 侧追踪信息(用于记录真实成本)
|
||||
provider_id=provider_id,
|
||||
provider_endpoint_id=endpoint_id,
|
||||
@@ -3249,7 +3249,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
||||
client_response_headers={"content-type": "application/json"},
|
||||
# 格式转换追踪
|
||||
endpoint_api_format=provider_api_format or None,
|
||||
has_format_conversion=needs_conversion,
|
||||
has_format_conversion=is_format_converted(provider_api_format, str(api_format)),
|
||||
# 模型映射信息
|
||||
target_model=mapped_model_result,
|
||||
request_metadata=request_metadata,
|
||||
@@ -3364,7 +3364,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
||||
first_byte_time_ms=ctx.first_byte_time_ms,
|
||||
api_format=ctx.api_format,
|
||||
endpoint_api_format=ctx.provider_api_format or None,
|
||||
has_format_conversion=ctx.needs_conversion,
|
||||
has_format_conversion=ctx.has_format_conversion,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.request_id}] 同步更新 streaming 状态失败: {e}")
|
||||
|
||||
@@ -18,6 +18,18 @@ if TYPE_CHECKING:
|
||||
from src.core.api_format.conversion.stream_state import StreamState
|
||||
|
||||
|
||||
def is_format_converted(
|
||||
provider_api_format: str | None,
|
||||
client_api_format: str | None,
|
||||
) -> bool:
|
||||
"""client 与 provider 的 api_format 是否真正不同(用于 usage 展示层)"""
|
||||
return bool(
|
||||
provider_api_format
|
||||
and client_api_format
|
||||
and provider_api_format.strip().lower() != client_api_format.strip().lower()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamContext:
|
||||
"""
|
||||
@@ -234,6 +246,15 @@ class StreamContext:
|
||||
if self.first_byte_time_ms is None:
|
||||
self.first_byte_time_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
@property
|
||||
def has_format_conversion(self) -> bool:
|
||||
"""是否发生了真正的格式转换(client 和 provider 的 api_format 不同)
|
||||
|
||||
区别于 needs_conversion:后者包含 envelope rewrite(如 Antigravity v1internal),
|
||||
不代表客户端与上游的数据格式真正不同。此属性用于 usage 展示层。
|
||||
"""
|
||||
return is_format_converted(self.provider_api_format, self.client_api_format)
|
||||
|
||||
def is_success(self) -> bool:
|
||||
"""检查请求是否成功"""
|
||||
return self.status_code < 400
|
||||
|
||||
@@ -108,6 +108,115 @@ class StreamProcessor:
|
||||
pass
|
||||
return self.default_parser
|
||||
|
||||
@staticmethod
|
||||
def _maybe_mark_gemini_completion(ctx: StreamContext, data: dict[str, Any]) -> None:
|
||||
"""Gemini: mark completion based on candidates[].finishReason."""
|
||||
candidates = data.get("candidates")
|
||||
if not isinstance(candidates, list) or not candidates:
|
||||
return
|
||||
for candidate in candidates:
|
||||
if not isinstance(candidate, dict):
|
||||
continue
|
||||
finish_reason = candidate.get("finishReason")
|
||||
if finish_reason is None:
|
||||
continue
|
||||
# UNSPECIFIED is the only clear "not done" sentinel across Gemini variants.
|
||||
if str(finish_reason) != "FINISH_REASON_UNSPECIFIED":
|
||||
ctx.has_completion = True
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def _extract_antigravity_usage_from_gemini_event(data: dict[str, Any]) -> dict[str, int] | None:
|
||||
"""Antigravity: lenient Gemini usage extraction (totalTokenCount may be missing)."""
|
||||
usage_metadata = data.get("usageMetadata", {})
|
||||
if not isinstance(usage_metadata, dict) or not usage_metadata:
|
||||
return None
|
||||
|
||||
def _as_int(v: Any) -> int:
|
||||
try:
|
||||
return int(v or 0)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
prompt = _as_int(usage_metadata.get("promptTokenCount"))
|
||||
cached = _as_int(usage_metadata.get("cachedContentTokenCount"))
|
||||
candidates = _as_int(usage_metadata.get("candidatesTokenCount"))
|
||||
thoughts = _as_int(usage_metadata.get("thoughtsTokenCount"))
|
||||
|
||||
# Align with Gemini billing convention: input_tokens includes cached content.
|
||||
return {
|
||||
"input_tokens": max(0, prompt),
|
||||
"output_tokens": max(0, candidates + thoughts),
|
||||
"cache_creation_tokens": 0,
|
||||
"cache_read_tokens": max(0, cached),
|
||||
}
|
||||
|
||||
def _unwrap_provider_envelope(self, ctx: StreamContext, data: dict[str, Any]) -> dict[str, Any]:
|
||||
behavior = get_provider_behavior(
|
||||
provider_type=str(getattr(ctx, "provider_type", "") or ""),
|
||||
endpoint_sig=str(getattr(ctx, "provider_api_format", "") or ""),
|
||||
)
|
||||
envelope = behavior.envelope
|
||||
if not envelope:
|
||||
return data
|
||||
|
||||
try:
|
||||
unwrapped = envelope.unwrap_response(data)
|
||||
envelope.postprocess_unwrapped_response(
|
||||
model=str(getattr(ctx, "model", "") or ""),
|
||||
data=unwrapped,
|
||||
)
|
||||
return unwrapped if isinstance(unwrapped, dict) else data
|
||||
except Exception:
|
||||
return data
|
||||
|
||||
def _update_ctx_from_provider_event(
|
||||
self,
|
||||
ctx: StreamContext,
|
||||
data: dict[str, Any],
|
||||
*,
|
||||
already_unwrapped: bool = False,
|
||||
) -> None:
|
||||
# Unwrap provider-specific envelopes (e.g. Antigravity v1internal wrapper)
|
||||
if not already_unwrapped:
|
||||
data = self._unwrap_provider_envelope(ctx, data)
|
||||
|
||||
parser = self.get_parser_for_provider(ctx)
|
||||
|
||||
# Provider usage extraction (best-effort)
|
||||
provider_type = str(getattr(ctx, "provider_type", "") or "").lower()
|
||||
provider_format = str(getattr(ctx, "provider_api_format", "") or "").strip().lower()
|
||||
|
||||
usage: dict[str, int] | None = None
|
||||
if provider_type == "antigravity" and provider_format.startswith("gemini:"):
|
||||
usage = self._extract_antigravity_usage_from_gemini_event(data)
|
||||
if usage is None:
|
||||
try:
|
||||
usage = parser.extract_usage_from_response(data)
|
||||
except Exception:
|
||||
usage = None
|
||||
|
||||
if usage:
|
||||
ctx.update_usage(
|
||||
input_tokens=usage.get("input_tokens"),
|
||||
output_tokens=usage.get("output_tokens"),
|
||||
cached_tokens=usage.get("cache_read_tokens"),
|
||||
cache_creation_tokens=usage.get("cache_creation_tokens"),
|
||||
)
|
||||
|
||||
# Provider completion detection (Gemini doesn't emit response.completed).
|
||||
if provider_format.startswith("gemini:"):
|
||||
self._maybe_mark_gemini_completion(ctx, data)
|
||||
|
||||
# Provider text extraction (optional)
|
||||
if self.collect_text:
|
||||
try:
|
||||
text = parser.extract_text_content(data)
|
||||
except Exception:
|
||||
text = ""
|
||||
if text:
|
||||
ctx.append_text(text)
|
||||
|
||||
def handle_sse_event(
|
||||
self,
|
||||
ctx: StreamContext,
|
||||
@@ -115,6 +224,7 @@ class StreamProcessor:
|
||||
data_str: str,
|
||||
*,
|
||||
skip_record: bool = False,
|
||||
skip_ctx_update: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
处理单个 SSE 事件
|
||||
@@ -126,6 +236,7 @@ class StreamProcessor:
|
||||
event_name: 事件名称
|
||||
data_str: 事件数据字符串
|
||||
skip_record: 是否跳过记录到 parsed_chunks(当需要格式转换时应为 True)
|
||||
skip_ctx_update: 跳过 usage/completion/text 提取(由调用方统一处理时使用)
|
||||
"""
|
||||
if not data_str:
|
||||
return
|
||||
@@ -142,31 +253,17 @@ class StreamProcessor:
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
|
||||
# Update usage/completion/text from provider event (envelope-aware).
|
||||
# 在 needs_conversion 正常流中由 _emit_converted_line 统一处理,此处跳过以避免重复。
|
||||
if not skip_ctx_update:
|
||||
self._update_ctx_from_provider_event(ctx, data)
|
||||
|
||||
# 统计数据事件数量(当需要格式转换时跳过,由 _emit_converted_line 统计/记录转换后的数据)
|
||||
if not skip_record:
|
||||
ctx.data_count += 1
|
||||
if ctx.record_parsed_chunks:
|
||||
ctx.parsed_chunks.append(data)
|
||||
|
||||
# 根据 Provider 格式选择解析器
|
||||
parser = self.get_parser_for_provider(ctx)
|
||||
|
||||
# 使用解析器提取 usage
|
||||
usage = parser.extract_usage_from_response(data)
|
||||
if usage:
|
||||
ctx.update_usage(
|
||||
input_tokens=usage.get("input_tokens"),
|
||||
output_tokens=usage.get("output_tokens"),
|
||||
cached_tokens=usage.get("cache_read_tokens"),
|
||||
cache_creation_tokens=usage.get("cache_creation_tokens"),
|
||||
)
|
||||
|
||||
# 提取文本
|
||||
if self.collect_text:
|
||||
text = parser.extract_text_content(data)
|
||||
if text:
|
||||
ctx.append_text(text)
|
||||
|
||||
# 检查完成
|
||||
event_type = event_name or data.get("type", "")
|
||||
if event_type in ("response.completed", "message_stop"):
|
||||
@@ -476,14 +573,31 @@ class StreamProcessor:
|
||||
self.on_streaming_start()
|
||||
streaming_started = True
|
||||
|
||||
def _process_line_with_perf(line: str, *, skip_record: bool = False) -> None:
|
||||
def _process_line_with_perf(
|
||||
line: str,
|
||||
*,
|
||||
skip_record: bool = False,
|
||||
skip_ctx_update: bool = False,
|
||||
) -> None:
|
||||
nonlocal parse_time
|
||||
if perf_capture:
|
||||
t0 = time.perf_counter()
|
||||
self._process_line(ctx, sse_parser, line, skip_record=skip_record)
|
||||
self._process_line(
|
||||
ctx,
|
||||
sse_parser,
|
||||
line,
|
||||
skip_record=skip_record,
|
||||
skip_ctx_update=skip_ctx_update,
|
||||
)
|
||||
parse_time += time.perf_counter() - t0
|
||||
return
|
||||
self._process_line(ctx, sse_parser, line, skip_record=skip_record)
|
||||
self._process_line(
|
||||
ctx,
|
||||
sse_parser,
|
||||
line,
|
||||
skip_record=skip_record,
|
||||
skip_ctx_update=skip_ctx_update,
|
||||
)
|
||||
|
||||
def _build_stream_error_payload(message: str) -> dict:
|
||||
if client_family == "openai":
|
||||
@@ -589,6 +703,14 @@ class StreamProcessor:
|
||||
data=data_obj,
|
||||
)
|
||||
|
||||
# Update usage/completion/text based on the unwrapped provider event.
|
||||
if isinstance(data_obj, dict):
|
||||
self._update_ctx_from_provider_event(
|
||||
ctx,
|
||||
data_obj,
|
||||
already_unwrapped=True,
|
||||
)
|
||||
|
||||
try:
|
||||
converted_events = registry.convert_stream_chunk(
|
||||
data_obj,
|
||||
@@ -648,7 +770,9 @@ class StreamProcessor:
|
||||
|
||||
if line:
|
||||
# 需要格式转换时,跳过记录原始数据(由 _emit_converted_line 记录转换后的数据)
|
||||
_process_line_with_perf(line, skip_record=True)
|
||||
_process_line_with_perf(
|
||||
line, skip_record=True, skip_ctx_update=True
|
||||
)
|
||||
normalized_line = line.rstrip("\r\n") if line else ""
|
||||
out_chunks = _emit_converted_line(normalized_line)
|
||||
if not out_chunks:
|
||||
@@ -878,6 +1002,7 @@ class StreamProcessor:
|
||||
line: str,
|
||||
*,
|
||||
skip_record: bool = False,
|
||||
skip_ctx_update: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
处理单行数据
|
||||
@@ -887,6 +1012,7 @@ class StreamProcessor:
|
||||
sse_parser: SSE 解析器
|
||||
line: 原始行数据
|
||||
skip_record: 是否跳过记录到 parsed_chunks(当需要格式转换时应为 True)
|
||||
skip_ctx_update: 跳过 usage/completion/text 提取(由调用方统一处理时使用)
|
||||
"""
|
||||
# SSEEventParser 以"去掉换行符"的单行文本作为输入;这里统一剔除 CR/LF,
|
||||
# 避免把空行误判成 "\n" 并导致事件边界解析错误。
|
||||
@@ -898,7 +1024,11 @@ class StreamProcessor:
|
||||
|
||||
for event in events:
|
||||
self.handle_sse_event(
|
||||
ctx, event.get("event"), event.get("data") or "", skip_record=skip_record
|
||||
ctx,
|
||||
event.get("event"),
|
||||
event.get("data") or "",
|
||||
skip_record=skip_record,
|
||||
skip_ctx_update=skip_ctx_update,
|
||||
)
|
||||
|
||||
async def create_monitored_stream(
|
||||
|
||||
@@ -227,7 +227,7 @@ class StreamTelemetryRecorder:
|
||||
request_type="chat",
|
||||
metadata=metadata,
|
||||
endpoint_api_format=ctx.provider_api_format,
|
||||
has_format_conversion=ctx.needs_conversion,
|
||||
has_format_conversion=ctx.has_format_conversion,
|
||||
)
|
||||
|
||||
logger.debug(f"{self.format_id} 流式响应完成")
|
||||
@@ -273,7 +273,7 @@ class StreamTelemetryRecorder:
|
||||
request_type="chat",
|
||||
metadata=metadata,
|
||||
endpoint_api_format=ctx.provider_api_format,
|
||||
has_format_conversion=ctx.needs_conversion,
|
||||
has_format_conversion=ctx.has_format_conversion,
|
||||
)
|
||||
|
||||
logger.debug(f"{self.format_id} 流式响应中断")
|
||||
@@ -320,7 +320,7 @@ class StreamTelemetryRecorder:
|
||||
request_type="chat",
|
||||
metadata=metadata,
|
||||
endpoint_api_format=ctx.provider_api_format,
|
||||
has_format_conversion=ctx.needs_conversion,
|
||||
has_format_conversion=ctx.has_format_conversion,
|
||||
)
|
||||
|
||||
logger.debug(f"{self.format_id} 流式响应被客户端取消")
|
||||
|
||||
@@ -106,7 +106,7 @@ FIXED_PROVIDERS: dict[ProviderType, FixedProviderTemplate] = {
|
||||
provider_type=ProviderType.ANTIGRAVITY,
|
||||
display_name="Antigravity",
|
||||
api_base_url=ANTIGRAVITY_PROD_URL,
|
||||
endpoint_signatures=["gemini:cli"],
|
||||
endpoint_signatures=["gemini:chat"],
|
||||
oauth=FixedProviderOAuth(
|
||||
authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
|
||||
token_url="https://oauth2.googleapis.com/token",
|
||||
|
||||
@@ -303,7 +303,9 @@ class ModelFetchScheduler:
|
||||
# Use request_builder's lazy refresh logic and persist refreshed token back to DB.
|
||||
# Endpoint signature is only used for tracing/debug; auth logic doesn't depend on it.
|
||||
endpoint_api_format = (
|
||||
"gemini:cli" if prepared.provider_type.lower() == ProviderType.ANTIGRAVITY else None
|
||||
"gemini:chat"
|
||||
if prepared.provider_type.lower() == ProviderType.ANTIGRAVITY
|
||||
else None
|
||||
)
|
||||
try:
|
||||
resolved = await resolve_oauth_access_token(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Antigravity v1internal request/response envelope helpers.
|
||||
|
||||
Antigravity reuses the `gemini:cli` endpoint signature but wraps the actual
|
||||
Antigravity reuses the `gemini:chat` endpoint signature but wraps the actual
|
||||
wire format:
|
||||
- Request: V1InternalRequest (top-level metadata + nested GeminiRequest)
|
||||
- Response: V1InternalResponse (top-level responseId + nested GeminiResponse)
|
||||
|
||||
@@ -249,7 +249,7 @@ async def fetch_models_antigravity(
|
||||
"id": model_id,
|
||||
"owned_by": "antigravity",
|
||||
"display_name": display_name,
|
||||
"api_format": "gemini:cli",
|
||||
"api_format": "gemini:chat",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -324,10 +324,14 @@ def register_all() -> None:
|
||||
from src.services.provider.transport import register_transport_hook
|
||||
|
||||
# Envelope
|
||||
register_envelope("antigravity", "gemini:chat", antigravity_v1internal_envelope)
|
||||
# Backward compat: allow existing endpoints that still use the old signature.
|
||||
register_envelope("antigravity", "gemini:cli", antigravity_v1internal_envelope)
|
||||
register_envelope("antigravity", "", antigravity_v1internal_envelope)
|
||||
|
||||
# Transport
|
||||
register_transport_hook("antigravity", "gemini:chat", build_antigravity_url)
|
||||
# Backward compat: allow existing endpoints that still use the old signature.
|
||||
register_transport_hook("antigravity", "gemini:cli", build_antigravity_url)
|
||||
|
||||
# Auth
|
||||
|
||||
@@ -106,7 +106,7 @@ def cache_result(
|
||||
if cached:
|
||||
try:
|
||||
result = json.loads(cached)
|
||||
logger.debug(f"缓存命中: {cache_key}")
|
||||
logger.trace(f"缓存命中: {cache_key}")
|
||||
return result
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"缓存解析失败,删除损坏缓存: {cache_key}, 错误: {e}")
|
||||
@@ -123,7 +123,7 @@ def cache_result(
|
||||
await redis_client.setex(
|
||||
cache_key, ttl, json.dumps(result, ensure_ascii=False, default=str)
|
||||
)
|
||||
logger.debug(f"缓存已保存: {cache_key}, TTL: {ttl}s")
|
||||
logger.trace(f"缓存已保存: {cache_key}, TTL: {ttl}s")
|
||||
except Exception as e:
|
||||
logger.warning(f"保存缓存失败: {e}")
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def test_convert_sse_line_unwraps_before_convert_stream_chunk() -> None:
|
||||
|
||||
ctx = StreamContext(model="gemini-2.0-flash", api_format="gemini:cli")
|
||||
ctx.provider_type = "antigravity"
|
||||
ctx.provider_api_format = "gemini:cli"
|
||||
ctx.provider_api_format = "gemini:chat"
|
||||
ctx.client_api_format = "gemini:cli"
|
||||
|
||||
v1_line = (
|
||||
|
||||
@@ -32,7 +32,7 @@ def test_antigravity_converts_claude_thinking_block_to_gemini_thought_part_prefe
|
||||
}
|
||||
|
||||
registry = get_format_converter_registry()
|
||||
out = registry.convert_request(req, "claude:chat", "gemini:cli", target_variant="antigravity")
|
||||
out = registry.convert_request(req, "claude:chat", "gemini:chat", target_variant="antigravity")
|
||||
|
||||
assert isinstance(out.get("contents"), list)
|
||||
model_turn = out["contents"][1]
|
||||
@@ -63,7 +63,7 @@ def test_antigravity_uses_dummy_signature_for_gemini_when_missing() -> None:
|
||||
}
|
||||
|
||||
registry = get_format_converter_registry()
|
||||
out = registry.convert_request(req, "claude:chat", "gemini:cli", target_variant="antigravity")
|
||||
out = registry.convert_request(req, "claude:chat", "gemini:chat", target_variant="antigravity")
|
||||
|
||||
parts = out["contents"][1]["parts"]
|
||||
assert parts[0]["thought"] is True
|
||||
@@ -91,7 +91,7 @@ def test_antigravity_drops_unsigned_thinking_for_non_gemini_models() -> None:
|
||||
}
|
||||
|
||||
registry = get_format_converter_registry()
|
||||
out = registry.convert_request(req, "claude:chat", "gemini:cli", target_variant="antigravity")
|
||||
out = registry.convert_request(req, "claude:chat", "gemini:chat", target_variant="antigravity")
|
||||
|
||||
parts = out["contents"][1]["parts"]
|
||||
assert all(p.get("thought") is not True for p in parts)
|
||||
@@ -111,7 +111,7 @@ def test_antigravity_inserts_dummy_thought_for_last_assistant_when_thinking_enab
|
||||
}
|
||||
|
||||
registry = get_format_converter_registry()
|
||||
out = registry.convert_request(req, "claude:chat", "gemini:cli", target_variant="antigravity")
|
||||
out = registry.convert_request(req, "claude:chat", "gemini:chat", target_variant="antigravity")
|
||||
|
||||
parts = out["contents"][1]["parts"]
|
||||
assert parts[0]["thought"] is True
|
||||
|
||||
@@ -19,7 +19,7 @@ class _DummyEndpoint:
|
||||
def test_antigravity_uses_v1internal_path_and_sets_contextvar() -> None:
|
||||
endpoint = _DummyEndpoint(
|
||||
base_url="https://ignored.example.com",
|
||||
api_format="gemini:cli",
|
||||
api_format="gemini:chat",
|
||||
provider=SimpleNamespace(provider_type="antigravity"),
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ async def test_get_provider_auth_oauth_returns_decrypted_auth_config() -> None:
|
||||
"project_id": "project-1",
|
||||
}
|
||||
|
||||
endpoint = SimpleNamespace(api_format="gemini:cli")
|
||||
endpoint = SimpleNamespace(api_format="gemini:chat")
|
||||
key = SimpleNamespace(
|
||||
id="k1",
|
||||
auth_type="oauth",
|
||||
|
||||
Reference in New Issue
Block a user