Skip to content

Commit 61f5759

Browse files
authored
feat: add Azure OpenAI Content Filter Support (#340)
1 parent 85a0f47 commit 61f5759

File tree

13 files changed

+224
-5
lines changed

13 files changed

+224
-5
lines changed

openai-client/src/commonMain/kotlin/com.aallam.openai.client/extension/internal/ChatMessageAssembler.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ internal class ChatMessageAssembler {
1111
private val chatContent = StringBuilder()
1212
private var chatRole: ChatRole? = null
1313
private val toolCallsAssemblers = mutableMapOf<Int, ToolCallAssembler>()
14+
private var chatContentFilterOffsets = mutableListOf<ContentFilterOffsets>()
15+
private var chatContentFilterResults = mutableListOf<ContentFilterResults>()
1416

1517
/**
1618
* Merges a chat chunk into the chat message being assembled.
1719
*/
1820
fun merge(chunk: ChatChunk): ChatMessageAssembler {
19-
chunk.delta.run {
21+
chunk.delta?.run {
2022
role?.let { chatRole = it }
2123
content?.let { chatContent.append(it) }
2224
functionCall?.let { call ->
@@ -30,6 +32,12 @@ internal class ChatMessageAssembler {
3032
assembler.merge(toolCall)
3133
}
3234
}
35+
chunk.contentFilterOffsets?.also {
36+
chatContentFilterOffsets.add(it)
37+
}
38+
chunk.contentFilterResults?.also {
39+
chatContentFilterResults.add(it)
40+
}
3341
return this
3442
}
3543

@@ -39,6 +47,8 @@ internal class ChatMessageAssembler {
3947
fun build(): ChatMessage = chatMessage {
4048
this.role = chatRole
4149
this.content = chatContent.toString()
50+
this.contentFilterOffsets = chatContentFilterOffsets
51+
this.contentFilterResults = chatContentFilterResults
4252
if (chatFuncName.isNotEmpty() || chatFuncArgs.isNotEmpty()) {
4353
this.functionCall = FunctionCall(chatFuncName.toString(), chatFuncArgs.toString())
4454
this.name = chatFuncName.toString()

openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestChatChunk.kt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import com.aallam.openai.api.chat.ChatChunk
44
import com.aallam.openai.api.chat.ChatDelta
55
import com.aallam.openai.api.chat.ChatMessage
66
import com.aallam.openai.api.chat.ChatRole
7+
import com.aallam.openai.api.chat.ContentFilterOffsets
8+
import com.aallam.openai.api.chat.ContentFilterResult
9+
import com.aallam.openai.api.chat.ContentFilterResults
710
import com.aallam.openai.api.core.FinishReason
811
import com.aallam.openai.client.extension.mergeToChatMessage
912
import kotlin.test.Test
@@ -20,6 +23,8 @@ class TestChatChunk {
2023
role = ChatRole(role = "assistant"),
2124
content = ""
2225
),
26+
contentFilterOffsets = null,
27+
contentFilterResults = null,
2328
finishReason = null
2429
),
2530
ChatChunk(
@@ -28,6 +33,8 @@ class TestChatChunk {
2833
role = null,
2934
content = "The"
3035
),
36+
contentFilterOffsets = null,
37+
contentFilterResults = null,
3138
finishReason = null
3239
),
3340
ChatChunk(
@@ -36,6 +43,8 @@ class TestChatChunk {
3643
role = null,
3744
content = " World"
3845
),
46+
contentFilterOffsets = null,
47+
contentFilterResults = null,
3948
finishReason = null
4049
),
4150
ChatChunk(
@@ -44,6 +53,8 @@ class TestChatChunk {
4453
role = null,
4554
content = " Series"
4655
),
56+
contentFilterOffsets = null,
57+
contentFilterResults = null,
4758
finishReason = null
4859
),
4960
ChatChunk(
@@ -52,6 +63,8 @@ class TestChatChunk {
5263
role = null,
5364
content = " in"
5465
),
66+
contentFilterOffsets = null,
67+
contentFilterResults = null,
5568
finishReason = null
5669
),
5770
ChatChunk(
@@ -60,6 +73,8 @@ class TestChatChunk {
6073
role = null,
6174
content = " "
6275
),
76+
contentFilterOffsets = null,
77+
contentFilterResults = null,
6378
finishReason = null
6479
),
6580
ChatChunk(
@@ -68,6 +83,8 @@ class TestChatChunk {
6883
role = null,
6984
content = "202"
7085
),
86+
contentFilterOffsets = null,
87+
contentFilterResults = null,
7188
finishReason = null
7289
),
7390
ChatChunk(
@@ -76,6 +93,8 @@ class TestChatChunk {
7693
role = null,
7794
content = "0"
7895
),
96+
contentFilterOffsets = null,
97+
contentFilterResults = null,
7998
finishReason = null
8099
),
81100
ChatChunk(
@@ -84,6 +103,8 @@ class TestChatChunk {
84103
role = null,
85104
content = " is"
86105
),
106+
contentFilterOffsets = null,
107+
contentFilterResults = null,
87108
finishReason = null
88109
),
89110
ChatChunk(
@@ -92,6 +113,8 @@ class TestChatChunk {
92113
role = null,
93114
content = " being held"
94115
),
116+
contentFilterOffsets = null,
117+
contentFilterResults = null,
95118
finishReason = null
96119
),
97120
ChatChunk(
@@ -100,6 +123,8 @@ class TestChatChunk {
100123
role = null,
101124
content = " in"
102125
),
126+
contentFilterOffsets = null,
127+
contentFilterResults = null,
103128
finishReason = null
104129
),
105130
ChatChunk(
@@ -108,6 +133,8 @@ class TestChatChunk {
108133
role = null,
109134
content = " Texas"
110135
),
136+
contentFilterOffsets = null,
137+
contentFilterResults = null,
111138
finishReason = null
112139
),
113140
ChatChunk(
@@ -116,6 +143,8 @@ class TestChatChunk {
116143
role = null,
117144
content = "."
118145
),
146+
contentFilterOffsets = null,
147+
contentFilterResults = null,
119148
finishReason = null
120149
),
121150
ChatChunk(
@@ -124,6 +153,24 @@ class TestChatChunk {
124153
role = null,
125154
content = null
126155
),
156+
contentFilterOffsets = null,
157+
contentFilterResults = null,
158+
finishReason = FinishReason(value = "stop")
159+
),
160+
ChatChunk(
161+
index = 0,
162+
delta = null,
163+
contentFilterOffsets = ContentFilterOffsets(
164+
checkOffset = 1,
165+
startOffset = 1,
166+
endOffset = 1,
167+
),
168+
contentFilterResults = ContentFilterResults(
169+
hate = ContentFilterResult(
170+
filtered = false,
171+
severity = "high",
172+
)
173+
),
127174
finishReason = FinishReason(value = "stop")
128175
)
129176
)
@@ -132,6 +179,21 @@ class TestChatChunk {
132179
role = ChatRole.Assistant,
133180
content = "The World Series in 2020 is being held in Texas.",
134181
name = null,
182+
contentFilterResults = listOf(
183+
ContentFilterResults(
184+
hate = ContentFilterResult(
185+
filtered = false,
186+
severity = "high",
187+
)
188+
)
189+
),
190+
contentFilterOffsets = listOf(
191+
ContentFilterOffsets(
192+
checkOffset = 1,
193+
startOffset = 1,
194+
endOffset = 1,
195+
)
196+
),
135197
)
136198
assertEquals(chatMessage, message)
137199
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.aallam.openai.client
2+
3+
import com.aallam.openai.api.chat.ChatCompletionChunk
4+
import com.aallam.openai.api.file.FileSource
5+
import com.aallam.openai.client.internal.JsonLenient
6+
import com.aallam.openai.client.internal.TestFileSystem
7+
import com.aallam.openai.client.internal.testFilePath
8+
import kotlin.test.Test
9+
import okio.buffer
10+
11+
class TestChatCompletionChunk {
12+
@Test
13+
fun testContentFilterDeserialization() {
14+
val json = FileSource(path = testFilePath("json/azureContentFilterChunk.json"), fileSystem = TestFileSystem)
15+
val actualJson = json.source.buffer().readByteArray().decodeToString()
16+
JsonLenient.decodeFromString<ChatCompletionChunk>(actualJson)
17+
}
18+
19+
@Test
20+
fun testDeserialization() {
21+
val json = FileSource(path = testFilePath("json/chatChunk.json"), fileSystem = TestFileSystem)
22+
val actualJson = json.source.buffer().readByteArray().decodeToString()
23+
JsonLenient.decodeFromString<ChatCompletionChunk>(actualJson)
24+
}
25+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"choices": [
3+
{
4+
"content_filter_offsets": {
5+
"check_offset": 33188,
6+
"start_offset": 33188,
7+
"end_offset": 33557
8+
},
9+
"content_filter_results": {
10+
"hate": {
11+
"filtered": false,
12+
"severity": "safe"
13+
},
14+
"self_harm": {
15+
"filtered": false,
16+
"severity": "safe"
17+
},
18+
"sexual": {
19+
"filtered": false,
20+
"severity": "safe"
21+
},
22+
"violence": {
23+
"filtered": false,
24+
"severity": "safe"
25+
}
26+
},
27+
"finish_reason": null,
28+
"index": 0
29+
}
30+
],
31+
"created": 0,
32+
"id": "",
33+
"model": "",
34+
"object": ""
35+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"choices": [
3+
{
4+
"delta": {
5+
"content": " engineering"
6+
},
7+
"finish_reason": null,
8+
"index": 0
9+
}
10+
],
11+
"created": 1716855566,
12+
"id": "chatcmpl-9TeqkT3BJs5zXQq12b204deXcY5nj",
13+
"model": "gpt-4o-2024-05-13",
14+
"object": "chat.completion.chunk",
15+
"system_fingerprint": "fp_5f4bad809a"
16+
}

openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatChunk.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.aallam.openai.api.chat;
22

3-
import com.aallam.openai.api.BetaOpenAI
43
import com.aallam.openai.api.core.FinishReason
54
import kotlinx.serialization.SerialName
65
import kotlinx.serialization.Serializable
@@ -19,7 +18,17 @@ public data class ChatChunk(
1918
/**
2019
* The generated chat message.
2120
*/
22-
@SerialName("delta") public val delta: ChatDelta,
21+
@SerialName("delta") public val delta: ChatDelta? = null,
22+
23+
/**
24+
* Azure content filter offsets
25+
*/
26+
@SerialName("content_filter_offsets") public val contentFilterOffsets: ContentFilterOffsets? = null,
27+
28+
/**
29+
* Azure content filter results
30+
*/
31+
@SerialName("content_filter_results") public val contentFilterResults: ContentFilterResults? = null,
2332

2433
/**
2534
* The reason why OpenAI stopped generating.

openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatMessage.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ public data class ChatMessage(
4545
* Tool call ID.
4646
*/
4747
@SerialName("tool_call_id") public val toolCallId: ToolId? = null,
48+
49+
/**
50+
* Azure Content Filter Results
51+
*/
52+
@SerialName("content_filter_results") public val contentFilterResults: List<ContentFilterResults>? = null,
53+
54+
/**
55+
* Azure Content Filter Offsets
56+
*/
57+
@SerialName("content_filter_offsets") public val contentFilterOffsets: List<ContentFilterOffsets>? = null,
4858
) {
4959

5060
public constructor(
@@ -54,13 +64,17 @@ public data class ChatMessage(
5464
functionCall: FunctionCall? = null,
5565
toolCalls: List<ToolCall>? = null,
5666
toolCallId: ToolId? = null,
67+
contentFilterResults: List<ContentFilterResults>? = null,
68+
contentFilterOffsets: List<ContentFilterOffsets>? = null,
5769
) : this(
5870
role = role,
5971
messageContent = content?.let { TextContent(it) },
6072
name = name,
6173
functionCall = functionCall,
6274
toolCalls = toolCalls,
6375
toolCallId = toolCallId,
76+
contentFilterOffsets = contentFilterOffsets,
77+
contentFilterResults = contentFilterResults,
6478
)
6579

6680
public constructor(
@@ -70,13 +84,17 @@ public data class ChatMessage(
7084
functionCall: FunctionCall? = null,
7185
toolCalls: List<ToolCall>? = null,
7286
toolCallId: ToolId? = null,
87+
contentFilterResults: List<ContentFilterResults>? = null,
88+
contentFilterOffsets: List<ContentFilterOffsets>? = null,
7389
) : this(
7490
role = role,
7591
messageContent = content?.let { ListContent(it) },
7692
name = name,
7793
functionCall = functionCall,
7894
toolCalls = toolCalls,
7995
toolCallId = toolCallId,
96+
contentFilterOffsets = contentFilterOffsets,
97+
contentFilterResults = contentFilterResults,
8098
)
8199

82100
val content: String?
@@ -282,6 +300,16 @@ public class ChatMessageBuilder {
282300
*/
283301
public var toolCalls: List<ToolCall>? = null
284302

303+
/**
304+
* Azure content filter offsets
305+
*/
306+
public var contentFilterOffsets: List<ContentFilterOffsets>? = null
307+
308+
/**
309+
* Azure content filter results
310+
*/
311+
public var contentFilterResults: List<ContentFilterResults>? = null
312+
285313
/**
286314
* Tool call ID.
287315
*/
@@ -313,6 +341,8 @@ public class ChatMessageBuilder {
313341
functionCall = functionCall,
314342
toolCalls = toolCalls,
315343
toolCallId = toolCallId,
344+
contentFilterOffsets = contentFilterOffsets,
345+
contentFilterResults = contentFilterResults,
316346
)
317347
}
318348
}

0 commit comments

Comments
 (0)