mirror of
https://github.com/openai/codex.git
synced 2026-04-30 01:16:54 +00:00
Own Genie Responses transport in Agent bridge
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user