diff --git a/src/main/kotlin/com/osglab/account/features/gateway/providers/deepseek/DeepSeekResponsesClient.kt b/src/main/kotlin/com/osglab/account/features/gateway/providers/deepseek/DeepSeekResponsesClient.kt index f76933e..147c566 100644 --- a/src/main/kotlin/com/osglab/account/features/gateway/providers/deepseek/DeepSeekResponsesClient.kt +++ b/src/main/kotlin/com/osglab/account/features/gateway/providers/deepseek/DeepSeekResponsesClient.kt @@ -9,6 +9,7 @@ import com.osglab.account.features.gateway.models.ProviderOutput import com.osglab.account.features.gateway.models.ProviderUsage import com.osglab.account.features.gateway.models.TextProviderRequest import com.osglab.account.features.gateway.models.UsageMeter +import com.osglab.account.features.gateway.providers.ProviderUpstreamException import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.bearerAuth @@ -72,16 +73,16 @@ internal class DeepSeekSearchFallbackClient( } catch (failure: CancellationException) { throw failure } catch (failure: Exception) { + val upstreamStatus = (failure as? ProviderUpstreamException)?.upstreamStatus LOG.warn( - "DeepSeek search path failed requestId={} taskKind={} searchMode={} failureType={}", + "DeepSeek search path failed requestId={} taskKind={} searchMode={} " + + "failureType={} upstreamStatus={} fallback=chat_completions", request.requestId, request.executionPolicy.taskKind.name, request.executionPolicy.webSearch.name, failure::class.simpleName ?: "Exception", + upstreamStatus ?: "unknown", ) - if (request.executionPolicy.webSearch == GatewayWebSearchMode.REQUIRED) { - throw failure - } return fallback.complete( request.copy( executionPolicy = request.executionPolicy.copy( diff --git a/src/test/kotlin/com/osglab/account/features/gateway/providers/deepseek/DeepSeekClientTest.kt b/src/test/kotlin/com/osglab/account/features/gateway/providers/deepseek/DeepSeekClientTest.kt index c19bff0..94c00db 100644 --- a/src/test/kotlin/com/osglab/account/features/gateway/providers/deepseek/DeepSeekClientTest.kt +++ b/src/test/kotlin/com/osglab/account/features/gateway/providers/deepseek/DeepSeekClientTest.kt @@ -291,16 +291,28 @@ class DeepSeekClientTest : StringSpec({ } } - "does not return an offline answer when current information requires search" { + "falls back to a guarded answer when required search fails" { val paths = mutableListOf() + val requestBodies = mutableListOf() val client = HttpClient( MockEngine { request -> paths += request.url.encodedPath - respond( - content = """{"error":{"message":"search unavailable"}}""", - status = HttpStatusCode.ServiceUnavailable, - headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), - ) + requestBodies += request.body.toByteArray().decodeToString() + if (request.url.encodedPath.endsWith("/responses")) { + respond( + content = """{"error":{"message":"search unavailable"}}""", + status = HttpStatusCode.ServiceUnavailable, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), + ) + } else { + respond( + content = + """{"choices":[{"message":{"content":"无法核实实时信息"}}],""" + + """"usage":{"prompt_tokens":9,"completion_tokens":4,"total_tokens":13}}""", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), + ) + } }, ) { install(ContentNegotiation) { @@ -308,17 +320,20 @@ class DeepSeekClientTest : StringSpec({ } } try { - shouldThrow { - DeepSeekProvider(client, CONFIG).execute( - request( - taskKind = GatewayTaskKind.CURRENT_INFORMATION_QUESTION, - webSearch = GatewayWebSearchMode.REQUIRED, - ), - DISCARD_OUTPUT, - ) - } + val usage = DeepSeekProvider(client, CONFIG).execute( + request( + taskKind = GatewayTaskKind.CURRENT_INFORMATION_QUESTION, + webSearch = GatewayWebSearchMode.REQUIRED, + ), + DISCARD_OUTPUT, + ) - paths shouldBe listOf("/v1/responses") + paths shouldBe listOf("/v1/responses", "/v1/chat/completions") + val fallbackSystem = Json.parseToJsonElement(requestBodies.last()).jsonObject + .getValue("messages").jsonArray.first().jsonObject + .getValue("content").jsonPrimitive.content + fallbackSystem shouldContain "could not be verified" + usage.units shouldBe 13 } finally { client.close() }