Own Genie Responses transport in Agent bridge

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-19 13:28:28 -07:00
parent f1a739e7eb
commit 4f6b0de73f
11 changed files with 312 additions and 100 deletions

View File

@@ -0,0 +1,143 @@
package com.openai.codexd
import android.content.Context
import android.util.Log
import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import org.json.JSONObject
object AgentResponsesProxy {
private const val TAG = "AgentResponsesProxy"
private const val CONNECT_TIMEOUT_MS = 30_000
private const val READ_TIMEOUT_MS = 30_000
private const val DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"
private const val DEFAULT_CHATGPT_BASE_URL = "https://chatgpt.com/backend-api/codex"
private const val DEFAULT_ORIGINATOR = "codex_cli_rs"
private const val DEFAULT_USER_AGENT = "codex_cli_rs/android_agent_bridge"
internal data class AuthSnapshot(
val authMode: String,
val bearerToken: String,
val accountId: String?,
)
fun sendResponsesRequest(
context: Context,
requestBody: String,
): CodexdLocalClient.HttpResponse {
val runtimeStatus = AgentCodexAppServerClient.readRuntimeStatus(
context = context,
refreshToken = true,
)
if (!runtimeStatus.authenticated) {
return CodexdLocalClient.HttpResponse(
statusCode = 401,
body = "not authenticated",
)
}
val authSnapshot = loadAuthSnapshot(File(context.filesDir, "codex-home/auth.json"))
val upstreamUrl = buildResponsesUrl(runtimeStatus.upstreamBaseUrl, authSnapshot.authMode)
Log.i(TAG, "Proxying /v1/responses -> $upstreamUrl (auth_mode=${authSnapshot.authMode})")
return executeRequest(upstreamUrl, requestBody, authSnapshot)
}
internal fun buildResponsesUrl(
upstreamBaseUrl: String,
authMode: String,
): String {
val normalizedBaseUrl = when {
upstreamBaseUrl.isBlank() || upstreamBaseUrl == "provider-default" -> {
if (authMode == "chatgpt") {
DEFAULT_CHATGPT_BASE_URL
} else {
DEFAULT_OPENAI_BASE_URL
}
}
else -> upstreamBaseUrl
}
return "${normalizedBaseUrl.trimEnd('/')}/responses"
}
internal fun loadAuthSnapshot(authFile: File): AuthSnapshot {
if (!authFile.isFile) {
throw IOException("Missing Agent auth file at ${authFile.absolutePath}")
}
val json = JSONObject(authFile.readText())
val openAiApiKey = json.stringOrNull("OPENAI_API_KEY")
val authMode = when (json.stringOrNull("auth_mode")) {
"apiKey", "apikey", "api_key" -> "apiKey"
"chatgpt", "chatgptAuthTokens", "chatgpt_auth_tokens" -> "chatgpt"
null -> if (openAiApiKey != null) "apiKey" else "chatgpt"
else -> if (openAiApiKey != null) "apiKey" else "chatgpt"
}
return if (authMode == "apiKey") {
val apiKey = openAiApiKey
?: throw IOException("Agent auth file is missing OPENAI_API_KEY")
AuthSnapshot(
authMode = authMode,
bearerToken = apiKey,
accountId = null,
)
} else {
val tokens = json.optJSONObject("tokens")
?: throw IOException("Agent auth file is missing chatgpt tokens")
val accessToken = tokens.stringOrNull("access_token")
?: throw IOException("Agent auth file is missing access_token")
AuthSnapshot(
authMode = "chatgpt",
bearerToken = accessToken,
accountId = tokens.stringOrNull("account_id"),
)
}
}
private fun executeRequest(
upstreamUrl: String,
requestBody: String,
authSnapshot: AuthSnapshot,
): CodexdLocalClient.HttpResponse {
val connection = (URL(upstreamUrl).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
connectTimeout = CONNECT_TIMEOUT_MS
readTimeout = READ_TIMEOUT_MS
doInput = true
doOutput = true
instanceFollowRedirects = true
setRequestProperty("Authorization", "Bearer ${authSnapshot.bearerToken}")
setRequestProperty("Content-Type", "application/json")
setRequestProperty("Accept", "text/event-stream")
setRequestProperty("Accept-Encoding", "identity")
setRequestProperty("originator", DEFAULT_ORIGINATOR)
setRequestProperty("User-Agent", DEFAULT_USER_AGENT)
if (authSnapshot.authMode == "chatgpt" && !authSnapshot.accountId.isNullOrBlank()) {
setRequestProperty("ChatGPT-Account-ID", authSnapshot.accountId)
}
}
return try {
connection.outputStream.use { output ->
output.write(requestBody.toByteArray(StandardCharsets.UTF_8))
output.flush()
}
val statusCode = connection.responseCode
val stream = if (statusCode >= 400) connection.errorStream else connection.inputStream
val responseBody = stream?.bufferedReader(StandardCharsets.UTF_8)?.use { it.readText() }
.orEmpty()
CodexdLocalClient.HttpResponse(
statusCode = statusCode,
body = responseBody,
)
} finally {
connection.disconnect()
}
}
private fun JSONObject.stringOrNull(key: String): String? {
if (!has(key) || isNull(key)) {
return null
}
return optString(key).ifBlank { null }
}
}