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 core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ kotlin {
implementation(libs.google.gson)
implementation(libs.google.cloud.storage)
implementation(libs.mcp)
implementation(libs.okhttp)
implementation(libs.kotlinx.coroutines.reactor)
implementation(libs.slf4j.api)
implementation(libs.google.flogger.extensions)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.adk.kt.serialization

import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
* Serializes a [ByteArray] as a base64 string.
*
* This matches the proto3-JSON representation of `bytes` fields, which is what the Vertex AI APIs
* (including the managed Session Service) expect. The kotlinx default encodes a `ByteArray` as a
* JSON array of numbers, which those APIs reject (`"Proto field is not repeating, cannot start
* list."`). Apply it with `@Serializable(with = Base64ByteArraySerializer::class)` on `ByteArray`
* fields that are persisted or sent over such wires (e.g. `Part.thoughtSignature`, `Blob.data`).
*/
@OptIn(ExperimentalEncodingApi::class)
object Base64ByteArraySerializer : KSerializer<ByteArray> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor(
"com.google.adk.kt.serialization.Base64ByteArray",
PrimitiveKind.STRING,
)

override fun serialize(encoder: Encoder, value: ByteArray) {
encoder.encodeString(Base64.encode(value))
}

override fun deserialize(decoder: Decoder): ByteArray = Base64.decode(decoder.decodeString())
}
3 changes: 2 additions & 1 deletion core/src/commonMain/kotlin/com/google/adk/kt/types/Blob.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@

package com.google.adk.kt.types

import com.google.adk.kt.serialization.Base64ByteArraySerializer
import kotlinx.serialization.Serializable

/** Represents binary data. */
@Serializable
data class Blob(
val mimeType: String? = null,
val displayName: String? = null,
val data: ByteArray? = null,
@Serializable(with = Base64ByteArraySerializer::class) val data: ByteArray? = null,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand Down
3 changes: 2 additions & 1 deletion core/src/commonMain/kotlin/com/google/adk/kt/types/Part.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.google.adk.kt.types

import com.google.adk.kt.annotations.FrameworkInternalApi
import com.google.adk.kt.serialization.Base64ByteArraySerializer
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
Expand Down Expand Up @@ -49,7 +50,7 @@ constructor(
/** Indicates whether the part represents the model's thought process. */
val thought: Boolean? = null,
/** An opaque signature for the thought. */
val thoughtSignature: ByteArray? = null,
@Serializable(with = Base64ByteArraySerializer::class) val thoughtSignature: ByteArray? = null,
/** Metadata for a video part (segment offsets and frame rate). */
val videoMetadata: VideoMetadata? = null,
/** Arbitrary key-value metadata associated with this part. The map must be JSON serializable. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.adk.kt.sessions

import com.google.auth.oauth2.GoogleCredentials
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response

/**
* A simplified HTTP client for interacting with the Vertex AI Session Service.
*
* @property project The Google Cloud Project ID.
* @property location The Google Cloud Location (region) where the service resides. The special
* value `"global"` selects the global, non-region-prefixed endpoint.
* @property httpClient The underlying [OkHttpClient] instance to use.
* @property credentials The Google credentials used for authentication.
* @property baseUrlOverride Optional override for the API base URL (useful for testing). When set,
* it must already include the API version segment (e.g. `http://localhost:8080/v1beta1`).
*/
class HttpApiClient(
private val project: String,
private val location: String,
private val httpClient: OkHttpClient = OkHttpClient(),
private val credentials: GoogleCredentials = defaultCredentials(),
private val baseUrlOverride: String? = null,
) {
private val baseUrl: String =
baseUrlOverride
?: if (location == "global") {
"https://aiplatform.googleapis.com/$API_VERSION"
} else {
"https://$location-aiplatform.googleapis.com/$API_VERSION"
}

/**
* Sends an authenticated HTTP request to the Vertex AI Session Service.
*
* This method refreshes the credentials if they are expired, injects the necessary
* `Authorization` and `Content-Type` headers (plus `x-goog-user-project` when the credentials
* declare a quota project), and executes the request synchronously.
*
* @param httpMethod The HTTP method to use (`GET`, `POST`, or `DELETE`).
* @param path The relative API path (e.g. `reasoningEngines/engine-id/sessions`).
* @param requestJson The JSON payload for `POST` requests. Ignored for `GET` and `DELETE`.
* @return The [Response] object from OkHttp. The caller is responsible for closing it.
* @throws IllegalArgumentException if the provided [httpMethod] is not supported.
*/
fun request(httpMethod: String, path: String, requestJson: String): Response {
val url = "$baseUrl/projects/$project/locations/$location/$path"
val requestBuilder = Request.Builder().url(url)

credentials.refreshIfExpired()
requestBuilder.header("Authorization", "Bearer ${credentials.accessToken.tokenValue}")
requestBuilder.header("Content-Type", "application/json")
credentials.quotaProjectId?.let { requestBuilder.header("x-goog-user-project", it) }

when (httpMethod.uppercase()) {
"POST" ->
requestBuilder.post(
requestJson.toRequestBody("application/json; charset=utf-8".toMediaType())
)
"GET" -> requestBuilder.get()
"DELETE" -> requestBuilder.delete()
else -> throw IllegalArgumentException("Unsupported HTTP method: $httpMethod")
}

return httpClient.newCall(requestBuilder.build()).execute()
}

companion object {
private const val API_VERSION = "v1beta1"

private fun defaultCredentials(): GoogleCredentials =
GoogleCredentials.getApplicationDefault()
.createScoped("https://www.googleapis.com/auth/cloud-platform")
}
}
Loading
Loading