Skip to content

Commit 690835d

Browse files
authored
feat(api): support Live sideband Decision commands (#1193)
Live Decision models are already present in the Java baseline, but `LiveSidebandConnection` has no typed `send` overloads for the three Decision commands. Add overloads for `decision.create`, `decision.item.create`, and `decision.item.delete`, and regenerate the API-reference input to include three client and three server Decision events in the Live reference groups. The residual change preserves the generated Java APIs and existing service customizations from [#1213](#1213). The generated-file inventory and generated SDK source are unchanged. Regression tests cover sideband transport, omitted list handling, and delegation visitor defaults. Validation on the promotion source: 15 focused tests passed; the trusted baseline custom-code check passed at 2,021 / 3,000 lines against the verified generated snapshot. See the checks on this PR for hosted results. Co-authored-by: yoni-oai <338284798+yoni-oai@users.noreply.github.com>
1 parent 71916f9 commit 690835d

6 files changed

Lines changed: 208 additions & 5 deletions

File tree

‎.castiron.stats.yml‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
schema_version: 1
2-
generation_id: b493525e-caf3-4075-95fd-5472a5a73a7a
3-
openapi_spec_hash: c9f4a7dead33c83456491dc0f08be686
4-
openapi_transformed_spec_hash: 04951af56e51b359c1993cc518a3892f
2+
generation_id: a13c16f0-9cec-4e2d-926e-e84061d97ed5
3+
openapi_spec_hash: 7fc94110c3530672f1e30a3eb2b37ac8
4+
openapi_transformed_spec_hash: 151abddd75bd1cf668f2f9129200e28d
55
config_hash: aae16cbb4861ebb9087774c3bce003a5
6-
codegen_sha: 9ce8f7587db9087dd5c8a8aabc0c01fd740c1b0a
6+
codegen_sha: a9c6e4417121e38b96e483c0ac9dce3437a719a9
77
codegen_hash: a11a90342d2dca2e925e24b5d8e86ce491b7da65622069f19eecef36f4e93cc7
8-
public_codegen_sha: cded9be95af5e873d65e176e01a00a264ef32682
8+
public_codegen_sha: 2774a763910a830fe9b2946f8ef457384c2fc774

‎api_reference/openapi.transformed.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108937,6 +108937,15 @@ x-oaiMeta:
108937108937
- type: object
108938108938
key: LiveCommentaryAppendParam
108939108939
path: <auto>
108940+
- type: object
108941+
key: LiveDecisionCreateParam
108942+
path: <auto>
108943+
- type: object
108944+
key: LiveDecisionItemCreateParam
108945+
path: <auto>
108946+
- type: object
108947+
key: LiveDecisionItemDeleteParam
108948+
path: <auto>
108940108949
- type: object
108941108950
key: LiveResponseItemCreateParam
108942108951
path: <auto>
@@ -108999,6 +109008,15 @@ x-oaiMeta:
108999109008
- type: object
109000109009
key: LiveDelegationCreated
109001109010
path: <auto>
109011+
- type: object
109012+
key: LiveDecisionEvent
109013+
path: <auto>
109014+
- type: object
109015+
key: LiveDecisionItemCreated
109016+
path: <auto>
109017+
- type: object
109018+
key: LiveDecisionItemDeleted
109019+
path: <auto>
109002109020
- type: object
109003109021
key: LiveResponseEvent
109004109022
path: <auto>

‎openai-java-client-okhttp/src/test/kotlin/com/openai/client/okhttp/LiveSidebandConnectionTest.kt‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import com.openai.core.http.WebSocketWriteNotAttempted
1212
import com.openai.core.jsonMapper
1313
import com.openai.helpers.LiveSidebandConnection
1414
import com.openai.helpers.LiveWebSocketOptions
15+
import com.openai.models.live.DecisionCreateEvent
16+
import com.openai.models.live.DecisionEvent
17+
import com.openai.models.live.DecisionItemCreateEvent
18+
import com.openai.models.live.DecisionItemDeleteEvent
1519
import com.openai.models.live.InputAudioMuteEvent
1620
import com.openai.models.live.InputAudioUnmuteEvent
1721
import com.openai.models.live.OutputAudioFixturePlayEvent
@@ -76,6 +80,62 @@ class LiveSidebandConnectionTest {
7680
.baseUrl(server.url("/proxy/v1?inherited=one%2Ftwo&x=a%2Bb").toString())
7781
.build()
7882

83+
@Test
84+
fun decisionCommandsAndResultUseSideband() {
85+
MockWebServer().use { server ->
86+
val peer = Peer()
87+
server.enqueue(MockResponse().withWebSocketUpgrade(peer))
88+
OkHttpClient.builder().build().use { http ->
89+
LiveSidebandConnection.connect(options(server, http), "signaling").use { sideband ->
90+
sideband.send(DecisionCreateEvent.builder().eventId("request").build())
91+
sideband.send(
92+
DecisionItemCreateEvent.builder()
93+
.item(DecisionItemCreateEvent.Item.builder().content("fixture").build())
94+
.build()
95+
)
96+
sideband.send(DecisionItemDeleteEvent.builder().itemId("item-1").build())
97+
for (expected in
98+
listOf(
99+
"""{"type":"decision.create","event_id":"request"}""",
100+
"""{"type":"decision.item.create","item":{"type":"message","role":"user","content":"fixture"}}""",
101+
"""{"type":"decision.item.delete","item_id":"item-1"}""",
102+
)) {
103+
assertThat(mapper.readTree(peer.messages.poll(8, TimeUnit.SECONDS)))
104+
.isEqualTo(mapper.readTree(expected))
105+
}
106+
peer.socket
107+
.get(8, TimeUnit.SECONDS)
108+
.send(
109+
"""
110+
{
111+
"type":"decision.event","event_id":"result","delegation_id":null,
112+
"trigger_id":"trigger-1","source_event_id":"transcript-1",
113+
"source_event_type":"session.input_transcript.delta",
114+
"event":{"type":"decision.completed","decision":{
115+
"model":"fixture-model","answers":[{"type":"predicate","probability":0.75}],
116+
"usage":{"input_tokens":1,"input_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},
117+
"output_tokens":2,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":3}
118+
}}
119+
}
120+
"""
121+
.trimIndent()
122+
)
123+
val result = sideband.receive().asDecisionEvent()
124+
assertThat(result.delegationId()).isEmpty()
125+
assertThat(result.triggerId()).contains("trigger-1")
126+
assertThat(result.sourceEventId()).contains("transcript-1")
127+
assertThat(result.sourceEventType())
128+
.contains(DecisionEvent.SourceEventType.SESSION_INPUT_TRANSCRIPT_DELTA)
129+
assertThat(
130+
result.event().decision().answers().single().asPredicate().probability()
131+
)
132+
.isEqualTo(0.75)
133+
assertThat(result.event().decision().usage().totalTokens()).isEqualTo(3L)
134+
}
135+
}
136+
}
137+
}
138+
79139
@Test
80140
fun replayAndReflectedAudioArriveWithoutAnyFreshStartedEventOrStartupCommand() {
81141
for (async in listOf(false, true)) {

‎openai-java-core/src/main/kotlin/com/openai/helpers/LiveSidebandConnection.kt‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import com.google.errorprone.annotations.MustBeClosed
66
import com.openai.core.ClientOptions
77
import com.openai.core.RequestOptions
88
import com.openai.models.live.CommentaryAppendEvent
9+
import com.openai.models.live.DecisionCreateEvent
10+
import com.openai.models.live.DecisionItemCreateEvent
11+
import com.openai.models.live.DecisionItemDeleteEvent
912
import com.openai.models.live.InputAudioMuteEvent
1013
import com.openai.models.live.InputAudioUnmuteEvent
1114
import com.openai.models.live.InstructionsAppendEvent
@@ -103,6 +106,12 @@ private constructor(
103106

104107
fun send(event: CommentaryAppendEvent) = sendCommand(event)
105108

109+
fun send(event: DecisionCreateEvent) = sendCommand(event)
110+
111+
fun send(event: DecisionItemCreateEvent) = sendCommand(event)
112+
113+
fun send(event: DecisionItemDeleteEvent) = sendCommand(event)
114+
106115
fun send(event: ResponseItemCreateEvent) = sendCommand(event)
107116

108117
fun send(event: ResponseCreateEvent) = sendCommand(event)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.openai.models.live
2+
3+
import com.openai.core.JsonValue
4+
import com.openai.models.live.sessions.SessionAcceptParams
5+
import org.assertj.core.api.Assertions.assertThat
6+
import org.junit.jupiter.api.Test
7+
8+
class DecisionsDelegationVisitorCompatibilityTest {
9+
@Test
10+
fun existingVisitorHandlesDecisionsAsUnknown() {
11+
val visitor =
12+
object : SessionConfig.Delegation.Visitor<JsonValue?> {
13+
override fun visitClient(client: ClientDelegation): JsonValue? = null
14+
15+
override fun visitResponses(
16+
responses: SessionConfig.Delegation.Responses
17+
): JsonValue? = null
18+
19+
override fun unknown(json: JsonValue?) = json
20+
}
21+
val decisions =
22+
SessionConfig.Delegation.Decisions.builder()
23+
.decisions(
24+
DecisionsDelegationConfig.builder()
25+
.model("decision-model")
26+
.addPredicateQuestion("Is the customer asking for help?")
27+
.build()
28+
)
29+
.build()
30+
31+
assertThat(SessionConfig.Delegation.ofDecisions(decisions).accept(visitor))
32+
.isEqualTo(JsonValue.from(decisions))
33+
}
34+
35+
@Test
36+
fun decisionsVisitorMethodsAreJvmDefaults() {
37+
for ((visitor, decisions) in
38+
listOf(
39+
MediaSessionConfig.Delegation.Visitor::class.java to
40+
MediaSessionConfig.Delegation.Decisions::class.java,
41+
SessionConfig.Delegation.Visitor::class.java to
42+
SessionConfig.Delegation.Decisions::class.java,
43+
SessionResource.Delegation.Visitor::class.java to
44+
SessionResource.Delegation.Decisions::class.java,
45+
SessionUpdateConfig.Delegation.Visitor::class.java to
46+
SessionUpdateConfig.Delegation.Decisions::class.java,
47+
SessionAcceptParams.Session.Delegation.Visitor::class.java to
48+
SessionAcceptParams.Session.Delegation.Decisions::class.java,
49+
)) {
50+
assertThat(visitor.getMethod("visitDecisions", decisions).isDefault)
51+
.describedAs("%s.visitDecisions is a JVM default method", visitor.name)
52+
.isTrue()
53+
}
54+
}
55+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.openai.models.live
2+
3+
import com.openai.core.jsonMapper
4+
import com.openai.models.live.sessions.SessionAcceptParams
5+
import org.assertj.core.api.Assertions.assertThat
6+
import org.assertj.core.api.Assertions.assertThatIllegalStateException
7+
import org.junit.jupiter.api.Test
8+
import org.junit.jupiter.params.ParameterizedTest
9+
import org.junit.jupiter.params.provider.ValueSource
10+
11+
internal class LiveDecisionsOmittedListTest {
12+
private val trigger =
13+
DecisionTrigger.builder()
14+
.decisions(DecisionTrigger.Decisions.builder().addPredicateQuestion("Ready?").build())
15+
.eventType(DecisionTrigger.EventType.SESSION_INPUT_TRANSCRIPT_DELTA)
16+
.build()
17+
18+
@Test
19+
fun builtConfigCopyPreservesOmissionUntilFirstTrigger() {
20+
val config = SessionUpdateConfig.builder().build()
21+
22+
assertThat(config.toBuilder().build()._triggers().isMissing()).isTrue()
23+
assertThat(config.toBuilder().addTrigger(trigger).build().triggers())
24+
.contains(listOf(trigger))
25+
}
26+
27+
@Test
28+
fun acceptedSessionCopyAcceptsFirstTrigger() {
29+
val session =
30+
SessionAcceptParams.Session.builder()
31+
.model(SessionAcceptParams.Session.Model.GPT_LIVE_1)
32+
.build()
33+
34+
assertThat(session.toBuilder().addTrigger(trigger).build().triggers())
35+
.contains(listOf(trigger))
36+
}
37+
38+
@Test
39+
fun deserializedConfigCopyPreservesOmissionUntilFirstQuestion() {
40+
val config = jsonMapper().readValue("{}", DecisionsDelegationUpdateConfig::class.java)
41+
val question =
42+
DecisionsDelegationUpdateConfig.Question.ofPredicate(
43+
DecisionsDelegationUpdateConfig.Question.Predicate.builder()
44+
.instructions("Ready?")
45+
.build()
46+
)
47+
48+
assertThat(config.toBuilder().build()._questions().isMissing()).isTrue()
49+
assertThat(config.toBuilder().addQuestion(question).build().questions())
50+
.contains(listOf(question))
51+
}
52+
53+
@ParameterizedTest
54+
@ValueSource(strings = ["null", "\"not a list\""])
55+
fun copiedNonListTriggerValueRejectsAppend(jsonValue: String) {
56+
val config =
57+
jsonMapper().readValue("""{"triggers":$jsonValue}""", SessionUpdateConfig::class.java)
58+
59+
assertThatIllegalStateException().isThrownBy { config.toBuilder().addTrigger(trigger) }
60+
}
61+
}

0 commit comments

Comments
 (0)