Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,171 +1,9 @@
package com.theveloper.pixelplay.data.ai.provider

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.concurrent.TimeUnit
class DeepSeekAiClient(apiKey: String) : OpenAiCompatibleClient(apiKey) {

/**
* DeepSeek AI provider implementation
* Uses OpenAI-compatible API
*/
class DeepSeekAiClient(private val apiKey: String) : AiClient {

companion object {
private const val DEFAULT_DEEPSEEK_MODEL = "deepseek-chat"
private const val BASE_URL = "https://api.deepseek.com"
}

@Serializable
data class ChatMessage(val role: String, val content: String)

@Serializable
data class ChatRequest(
val model: String,
val messages: List<ChatMessage>,
val temperature: Double = 0.7
)

@Serializable
data class ChatChoice(val message: ChatMessage)

@Serializable
data class ChatResponse(val choices: List<ChatChoice>)

@Serializable
data class ModelItem(val id: String)

@Serializable
data class ModelsResponse(val data: List<ModelItem>)

private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()

private val json = Json {
ignoreUnknownKeys = true
isLenient = true
}

override suspend fun generateContent(
model: String,
systemPrompt: String,
prompt: String,
temperature: Float
): String {
return withContext(Dispatchers.IO) {
val resolvedModel = model.ifBlank { DEFAULT_DEEPSEEK_MODEL }
val messagesList = mutableListOf<ChatMessage>()
if (systemPrompt.isNotBlank()) {
messagesList.add(ChatMessage(role = "system", content = systemPrompt))
}
messagesList.add(ChatMessage(role = "user", content = prompt))

val requestBody = ChatRequest(
model = resolvedModel,
messages = messagesList,
temperature = temperature.toDouble()
)

val jsonBody = json.encodeToString(ChatRequest.serializer(), requestBody)
val body = jsonBody.toRequestBody("application/json".toMediaType())

val request = Request.Builder()
.url("$BASE_URL/chat/completions")
.addHeader("Authorization", "Bearer $apiKey")
.addHeader("Content-Type", "application/json")
.post(body)
.build()

try {
client.newCall(request).execute().use { response ->
val responseBody = response.body.string()

if (!response.isSuccessful) {
throw AiProviderSupport.createException(
providerName = "DeepSeek",
statusCode = response.code,
transportMessage = response.message,
responseBody = responseBody,
requestedModel = resolvedModel
)
}

val chatResponse = json.decodeFromString<ChatResponse>(responseBody)
chatResponse.choices.firstOrNull()?.message?.content
?: throw AiProviderSupport.createException(
providerName = "DeepSeek",
statusCode = response.code,
transportMessage = "Response had no content",
responseBody = responseBody,
requestedModel = resolvedModel
)
}
} catch (e: Exception) {
throw AiProviderSupport.wrapThrowable("DeepSeek", e, resolvedModel)
}
}
}

override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int {
// DeepSeek estimation
return (systemPrompt.length + prompt.length) / 4
}

override suspend fun getAvailableModels(apiKey: String): List<String> {
return withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
.url("$BASE_URL/models")
.addHeader("Authorization", "Bearer $apiKey")
.get()
.build()

val response = client.newCall(request).execute()

if (!response.isSuccessful) {
return@withContext getDefaultModels()
}

val responseBody = response.body.string()
val modelsResponse = json.decodeFromString<ModelsResponse>(responseBody)
modelsResponse.data.map { it.id }
} catch (e: Exception) {
getDefaultModels()
}
}
}

override suspend fun validateApiKey(apiKey: String): Boolean {
return withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
.url("$BASE_URL/models")
.addHeader("Authorization", "Bearer $apiKey")
.get()
.build()

val response = client.newCall(request).execute()
response.isSuccessful
} catch (e: Exception) {
false
}
}
}

override fun getDefaultModel(): String = DEFAULT_DEEPSEEK_MODEL

private fun getDefaultModels(): List<String> {
return listOf(
"deepseek-chat",
"deepseek-reasoner"
)
}
override val providerName = "DeepSeek"
override val baseUrl = "https://api.deepseek.com"
override val providerDefaultModel = "deepseek-chat"
override val providerDefaultModels = listOf("deepseek-chat", "deepseek-reasoner")
}
Original file line number Diff line number Diff line change
@@ -1,171 +1,30 @@
package com.theveloper.pixelplay.data.ai.provider

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.concurrent.TimeUnit

/**
* A generic AI client for OpenAI-compatible APIs (NVIDIA, Kimi, GLM, etc.)
*/
class GenericOpenAiClient(
private val apiKey: String,
private val baseUrl: String,
apiKey: String,
override val baseUrl: String,
private val defaultModelId: String,
private val providerName: String = "OpenAI"
) : AiClient {

@Serializable
private data class ChatMessage(val role: String, val content: String)

@Serializable
private data class ChatRequest(
val model: String,
val messages: List<ChatMessage>,
val temperature: Double = 0.7
)

@Serializable
private data class ChatChoice(val message: ChatMessage)

@Serializable
private data class ChatResponse(val choices: List<ChatChoice>)

@Serializable
private data class ModelItem(val id: String)

@Serializable
private data class ModelsResponse(val data: List<ModelItem>)

private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()

private val json = Json {
ignoreUnknownKeys = true
isLenient = true
}

override suspend fun generateContent(
model: String,
systemPrompt: String,
prompt: String,
temperature: Float
): String {
return withContext(Dispatchers.IO) {
val resolvedModel = model.ifBlank { defaultModelId }
val messagesList = mutableListOf<ChatMessage>()
if (systemPrompt.isNotBlank()) {
messagesList.add(ChatMessage(role = "system", content = systemPrompt))
}
messagesList.add(ChatMessage(role = "user", content = prompt))

val requestBody = ChatRequest(
model = resolvedModel,
messages = messagesList,
temperature = temperature.toDouble()
)

val jsonBody = json.encodeToString(ChatRequest.serializer(), requestBody)
val body = jsonBody.toRequestBody("application/json".toMediaType())

val requestBuilder = Request.Builder()
.url("${baseUrl.trimEnd('/')}/chat/completions")
.addHeader("Authorization", "Bearer $apiKey")
.addHeader("Content-Type", "application/json")

if (providerName.equals("OpenRouter", ignoreCase = true)) {
requestBuilder.addHeader("HTTP-Referer", "https://github.com/theovilardo/PixelPlayer")
requestBuilder.addHeader("X-Title", "PixelPlayer")
}

val request = requestBuilder.post(body).build()
override val providerName: String = "OpenAI"
) : OpenAiCompatibleClient(apiKey) {

try {
client.newCall(request).execute().use { response ->
val responseBody = response.body.string()
override val providerDefaultModel: String get() = defaultModelId
override val providerDefaultModels: List<String> get() = listOf(defaultModelId)

if (!response.isSuccessful) {
throw AiProviderSupport.createException(
providerName = providerName,
statusCode = response.code,
transportMessage = response.message,
responseBody = responseBody,
requestedModel = resolvedModel
)
}

val chatResponse = json.decodeFromString<ChatResponse>(responseBody)
chatResponse.choices.firstOrNull()?.message?.content
?: throw AiProviderSupport.createException(
providerName = providerName,
statusCode = response.code,
transportMessage = "Response had no content",
responseBody = responseBody,
requestedModel = resolvedModel
)
}
} catch (e: Exception) {
throw AiProviderSupport.wrapThrowable(providerName, e, resolvedModel)
}
override fun decorateRequest(builder: Request.Builder): Request.Builder {
if (providerName.equals("OpenRouter", ignoreCase = true)) {
builder.addHeader("HTTP-Referer", "https://github.com/theovilardo/PixelPlayer")
builder.addHeader("X-Title", "PixelPlayer")
}
return builder
}
Comment on lines +18 to 24

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 OpenRouter-specific headers only added for generateContent, not getAvailableModels/validateApiKey

The decorateRequest hook in GenericOpenAiClient.kt:18-24 adds OpenRouter-specific headers (HTTP-Referer, X-Title) but is only called from generateContent(). The getAvailableModels() and validateApiKey() methods in the base class don't call decorateRequest. This matches the pre-refactoring behavior (the old GenericOpenAiClient also only added those headers in generateContent), so it's not a regression. However, if OpenRouter requires these headers for all endpoints, this could cause model listing or key validation to fail for OpenRouter users.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


override suspend fun countTokens(model: String, systemPrompt: String, prompt: String): Int {
// Estimation for generic providers
return (systemPrompt.length + prompt.length) / 4
}

override suspend fun getAvailableModels(apiKey: String): List<String> {
return withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
.url("${baseUrl.trimEnd('/')}/models")
.addHeader("Authorization", "Bearer $apiKey")
.get()
.build()

val response = client.newCall(request).execute()

if (!response.isSuccessful) {
return@withContext listOf(defaultModelId)
}

val responseBody = response.body.string()
val modelsResponse = json.decodeFromString<ModelsResponse>(responseBody)
modelsResponse.data.map { it.id }.filter {
!it.contains("whisper") && !it.contains("embed") && !it.contains("tts")
}
} catch (e: Exception) {
listOf(defaultModelId)
}
}
}

override suspend fun validateApiKey(apiKey: String): Boolean {
return withContext(Dispatchers.IO) {
try {
// Try a simple models list check as validation
val request = Request.Builder()
.url("${baseUrl.trimEnd('/')}/models")
.addHeader("Authorization", "Bearer $apiKey")
.get()
.build()

val response = client.newCall(request).execute()
response.isSuccessful
} catch (e: Exception) {
false
}

override fun filterModels(models: List<String>): List<String> =
models.filter {
!it.contains("whisper") && !it.contains("embed") && !it.contains("tts")
}
}

override fun getDefaultModel(): String = defaultModelId
}
Loading
Loading