Skip to content
Open
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
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ dependencies {
dokka(project(":prompt:prompt-executor:prompt-executor-clients:prompt-executor-openai-client"))
dokka(project(":prompt:prompt-executor:prompt-executor-clients:prompt-executor-openai-client-base"))
dokka(project(":prompt:prompt-executor:prompt-executor-clients:prompt-executor-openrouter-client"))
dokka(project(":prompt:prompt-executor:prompt-executor-clients:prompt-executor-dashscope-client"))
dokka(project(":prompt:prompt-executor:prompt-executor-llms"))
dokka(project(":prompt:prompt-executor:prompt-executor-llms-all"))
dokka(project(":prompt:prompt-executor:prompt-executor-model"))
Expand Down
1 change: 1 addition & 0 deletions koog-agents/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ val included = setOf(
":prompt:prompt-executor:prompt-executor-clients:prompt-executor-openai-client",
":prompt:prompt-executor:prompt-executor-clients:prompt-executor-openai-client-base",
":prompt:prompt-executor:prompt-executor-clients:prompt-executor-openrouter-client",
":prompt:prompt-executor:prompt-executor-clients:prompt-executor-dashscope-client",
":prompt:prompt-executor:prompt-executor-llms",
":prompt:prompt-executor:prompt-executor-llms-all",
":prompt:prompt-executor:prompt-executor-model",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Module prompt-executor-dashscope-client

A client implementation for executing prompts using Alibaba Cloud's DashScope API with Qwen models. Provides OpenAI-compatible interface for seamless integration.

### Overview

This module provides a client implementation for the DashScope API, allowing you to execute prompts using Alibaba's Qwen models. It leverages OpenAI-compatible endpoints and handles authentication, request formatting, response parsing, and tool integration specific to DashScope's API requirements.

### Model-Specific Parameters Support

#### DashScope Parameters

The client supports DashScope-specific parameters through `DashscopeParams` class:

```kotlin
val dashscopeParams = DashscopeParams(
temperature = 0.7, // Controls randomness (0.0 to 2.0)
maxTokens = 1000, // Maximum tokens to generate
toolChoice = LLMParams.ToolChoice.AUTO // Tool usage control
)
```

### API Endpoints

The client connects to DashScope using OpenAI-compatible endpoints:

- **China mainland**: `https://dashscope.aliyuncs.com/compatible-mode/v1`
- **International**: `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`

### Using in your project

Add the dependency to your project:

```kotlin
dependencies {
implementation("ai.koog.prompt:prompt-executor-dashscope-client:$version")
}
```

Configure the client with your API key:

```kotlin
val dashscopeClient = DashscopeLLMClient(
apiKey = "your-dashscope-api-key",
)
```

### Configuration Options

```kotlin
// Default configuration (China mainland)
val client = DashscopeLLMClient(
apiKey = "your-api-key"
)

// International configuration
val internationalClient = DashscopeLLMClient(
apiKey = "your-api-key",
settings = DashscopeClientSettings(
baseUrl = "https://dashscope-intl.aliyuncs.com/",
timeoutConfig = ConnectionTimeoutConfig(
requestTimeoutMillis = 60_000,
connectTimeoutMillis = 30_000,
socketTimeoutMillis = 60_000
)
)
)
```

### Example of usage

```kotlin
suspend fun main() {
val client = DashscopeLLMClient(
apiKey = System.getenv("DASHSCOPE_API_KEY"),
)

// Basic text completion
val response = client.execute(
prompt = prompt {
system("你是一个有用的AI助手")
user("你好,请介绍一下自己")
},
model = DashscopeModels.QWEN_PLUS,
params = LLMParams(
temperature = 0.7,
maxTokens = 1000
)
)

println(response)
}
```

### Tool Usage Examples

```kotlin
// Using tools with DashScope
val responseWithTools = client.execute(
prompt = prompt {
system("You are a helpful assistant with access to tools")
user("What's the weather like in Beijing?")
},
model = DashscopeModels.QWEN_PLUS,
params = LLMParams(
temperature = 0.5,
toolChoice = LLMParams.ToolChoice.AUTO
),
tools = listOf(weatherTool) // Your defined tools
)

// Streaming response
val streamingResponse = client.executeStream(
prompt = prompt {
user("Write a short story about a robot learning to paint")
},
model = DashscopeModels.QWEN_FLASH,
params = LLMParams(
temperature = 0.8,
maxTokens = 2000
)
)

streamingResponse.collect { chunk ->
print(chunk)
}

// Using speculation for complex reasoning
val speculationResponse = client.execute(
prompt = prompt {
system("Analyze the following problem step by step")
user("How can we optimize database query performance for a social media platform?")
},
model = DashscopeModels.QWEN_PLUS, // Supports speculation capability
params = LLMParams(
temperature = 0.3,
maxTokens = 3000
)
)
```

### Advanced Configuration

```kotlin
// Custom timeout configuration
val customClient = DashscopeLLMClient(
apiKey = "your-api-key",
settings = DashscopeClientSettings(
baseUrl = "https://dashscope.aliyuncs.com/",
chatCompletionsPath = "compatible-mode/v1/chat/completions",
timeoutConfig = ConnectionTimeoutConfig(
requestTimeoutMillis = 120_000, // 2 minutes
connectTimeoutMillis = 30_000, // 30 seconds
socketTimeoutMillis = 120_000 // 2 minutes
)
),
baseClient = HttpClient {
// Custom HTTP client configuration
}
)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import ai.koog.gradle.publish.maven.Publishing.publishToMaven

group = rootProject.group
version = rootProject.version

plugins {
id("ai.kotlin.multiplatform")
alias(libs.plugins.kotlin.serialization)
}

kotlin {
sourceSets {
commonMain {
dependencies {
api(project(":prompt:prompt-executor:prompt-executor-clients:prompt-executor-openai-client-base"))
api(project(":prompt:prompt-structure"))
api(project(":prompt:prompt-executor:prompt-executor-clients:prompt-executor-openai-client"))
implementation(libs.oshai.kotlin.logging)
}
}

commonTest {
dependencies {
implementation(project(":test-utils"))
}
}

jvmTest {
dependencies {
implementation(libs.ktor.client.mock)
}
}
}

explicitApi()
}

publishToMaven()
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package ai.koog.prompt.executor.clients.dashscope

import ai.koog.prompt.dsl.ModerationResult
import ai.koog.prompt.dsl.Prompt
import ai.koog.prompt.executor.clients.ConnectionTimeoutConfig
import ai.koog.prompt.executor.clients.dashscope.models.DashscopeChatCompletionRequest
import ai.koog.prompt.executor.clients.dashscope.models.DashscopeChatCompletionRequestSerializer
import ai.koog.prompt.executor.clients.dashscope.models.DashscopeChatCompletionResponse
import ai.koog.prompt.executor.clients.dashscope.models.DashscopeChatCompletionStreamResponse
import ai.koog.prompt.executor.clients.openai.base.AbstractOpenAILLMClient
import ai.koog.prompt.executor.clients.openai.base.OpenAIBasedSettings
import ai.koog.prompt.executor.clients.openai.base.models.OpenAIMessage
import ai.koog.prompt.executor.clients.openai.base.models.OpenAITool
import ai.koog.prompt.executor.clients.openai.base.models.OpenAIToolChoice
import ai.koog.prompt.executor.clients.openai.structure.OpenAIBasicJsonSchemaGenerator
import ai.koog.prompt.executor.clients.openai.structure.OpenAIStandardJsonSchemaGenerator
import ai.koog.prompt.executor.model.LLMChoice
import ai.koog.prompt.llm.LLMProvider
import ai.koog.prompt.llm.LLModel
import ai.koog.prompt.params.LLMParams
import ai.koog.prompt.streaming.StreamFrameFlowBuilder
import ai.koog.prompt.structure.RegisteredBasicJsonSchemaGenerators
import ai.koog.prompt.structure.RegisteredStandardJsonSchemaGenerators
import ai.koog.prompt.structure.annotations.InternalStructuredOutputApi
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.HttpClient
import kotlinx.datetime.Clock

/**
* Configuration settings for connecting to the DashScope API using OpenAI-compatible endpoints.
*
* @property baseUrl The base URL of the DashScope API.
* For international: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
* For China mainland: "https://dashscope.aliyuncs.com/compatible-mode/v1"
* @property chatCompletionsPath The path for chat completions (default: "/chat/completions")
* @property timeoutConfig Configuration for connection timeouts including request, connection, and socket timeouts.
*/
public class DashscopeClientSettings(
baseUrl: String = "https://dashscope.aliyuncs.com/",
chatCompletionsPath: String = "compatible-mode/v1/chat/completions",
timeoutConfig: ConnectionTimeoutConfig = ConnectionTimeoutConfig()
) : OpenAIBasedSettings(baseUrl, chatCompletionsPath, timeoutConfig)

/**
* Implementation of [AbstractOpenAILLMClient] for DashScope API using OpenAI-compatible endpoints.
*
* @param apiKey The API key for the DashScope API
* @param settings The base URL, chat completion path, and timeouts for the DashScope API,
* defaults to "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" and 900s
* @param baseClient HTTP client for making requests
* @param clock Clock instance used for tracking response metadata timestamps
*/
public class DashscopeLLMClient(
apiKey: String,
private val settings: DashscopeClientSettings = DashscopeClientSettings(),
baseClient: HttpClient = HttpClient(),
clock: Clock = Clock.System
) : AbstractOpenAILLMClient<DashscopeChatCompletionResponse, DashscopeChatCompletionStreamResponse>(
apiKey,
settings,
baseClient,
clock,
staticLogger
) {

@OptIn(InternalStructuredOutputApi::class)
private companion object {
private val staticLogger = KotlinLogging.logger { }

init {
RegisteredBasicJsonSchemaGenerators[LLMProvider.Alibaba] = OpenAIBasicJsonSchemaGenerator
RegisteredStandardJsonSchemaGenerators[LLMProvider.Alibaba] = OpenAIStandardJsonSchemaGenerator
}
}

override fun serializeProviderChatRequest(
messages: List<OpenAIMessage>,
model: LLModel,
tools: List<OpenAITool>?,
toolChoice: OpenAIToolChoice?,
params: LLMParams,
stream: Boolean
): String {
val dashscopeParams = params.toDashscopeParams()
val responseFormat = createResponseFormat(params.schema, model)

val request = DashscopeChatCompletionRequest(
messages = messages,
model = model.id,
maxTokens = dashscopeParams.maxTokens,
responseFormat = responseFormat,
stream = stream,
temperature = dashscopeParams.temperature,
toolChoice = dashscopeParams.toolChoice?.toOpenAIToolChoice(),
tools = tools?.takeIf { it.isNotEmpty() },
logprobs = dashscopeParams.logprobs,
topLogprobs = dashscopeParams.topLogprobs,
topP = dashscopeParams.topP,
frequencyPenalty = dashscopeParams.frequencyPenalty,
presencePenalty = dashscopeParams.presencePenalty,
stop = dashscopeParams.stop,
enableSearch = dashscopeParams.enableSearch,
parallelToolCalls = dashscopeParams.parallelToolCalls,
enableThinking = dashscopeParams.enableThinking,
)

return json.encodeToString(DashscopeChatCompletionRequestSerializer, request)
}

override fun processProviderChatResponse(response: DashscopeChatCompletionResponse): List<LLMChoice> {
require(response.choices.isNotEmpty()) { "Empty choices in response" }
return response.choices.map {
it.message.toMessageResponses(
it.finishReason,
createMetaInfo(response.usage),
)
}
}

override fun decodeStreamingResponse(data: String): DashscopeChatCompletionStreamResponse =
json.decodeFromString(data)

override fun decodeResponse(data: String): DashscopeChatCompletionResponse =
json.decodeFromString(data)

override suspend fun StreamFrameFlowBuilder.processStreamingChunk(chunk: DashscopeChatCompletionStreamResponse) {
chunk.choices.firstOrNull()?.let { choice ->
choice.delta.content?.let { emitAppend(it) }
choice.delta.toolCalls?.forEach { toolCall ->
val index = toolCall.index
val id = toolCall.id
val name = toolCall.function?.name
val arguments = toolCall.function?.arguments
upsertToolCall(index, id, name, arguments)
}
choice.finishReason?.let { emitEnd(it, createMetaInfo(chunk.usage)) }
}
}

public override suspend fun moderate(prompt: Prompt, model: LLModel): ModerationResult {
logger.warn { "Moderation is not supported by DashScope API" }
throw UnsupportedOperationException("Moderation is not supported by DashScope API.")
}
}
Loading