feat: вычисление сессии по истории (LCP) для providers[].session_header
- SHA-256 на commonMain + SessionRegistry (LCP по префикс-хэшам, LRU 1000/6ч, Mutex) - providers[].session_header: прокси сам ставит/перезаписывает заголовок сессии - цепочка хэшей стартует с первого user-сообщения (system не склеивает сессии) - CONFIG.md + тесты (SHA-256, LCP, LRU/TTL, парсинг, стабильность префиксов)
This commit is contained in:
@@ -97,8 +97,9 @@ fun main() {
|
||||
}
|
||||
|
||||
val http = createHttpClient()
|
||||
val sessions = SessionRegistry()
|
||||
startServer(config.server.host, config.server.port) {
|
||||
proxyModule(config, providersById, upstreamsById, active, http)
|
||||
proxyModule(config, providersById, upstreamsById, active, sessions, http)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +110,12 @@ fun Application.proxyModule(
|
||||
providersById: Map<String, ProviderConf>,
|
||||
upstreamsById: Map<String, UpstreamConf>,
|
||||
active: Map<String, UpstreamCounter>,
|
||||
sessions: SessionRegistry,
|
||||
http: HttpClient,
|
||||
) {
|
||||
routing {
|
||||
post("/v1/chat/completions") {
|
||||
handleChat(call, config, providersById, upstreamsById, active, http)
|
||||
handleChat(call, config, providersById, upstreamsById, active, sessions, http)
|
||||
}
|
||||
get("/v1/models") {
|
||||
handleModels(call, config)
|
||||
@@ -127,6 +129,7 @@ private suspend fun handleChat(
|
||||
providersById: Map<String, ProviderConf>,
|
||||
upstreamsById: Map<String, UpstreamConf>,
|
||||
active: Map<String, UpstreamCounter>,
|
||||
sessions: SessionRegistry,
|
||||
http: HttpClient,
|
||||
) {
|
||||
log.info { "[llm-proxy] chat ${call.request.httpMethod.value} ${call.request.uri} headers: ${formatHeadersForLog(call.request.headers)}" }
|
||||
@@ -186,13 +189,19 @@ private suspend fun handleChat(
|
||||
|
||||
val url = provider.url.trimEnd('/') + "/chat/completions"
|
||||
val providerKey = resolveEnv(provider.key)
|
||||
val sessionHeader = provider.session_header
|
||||
// Клиентский одноимённый заголовок не пробрасываем: при метке
|
||||
// значение x-opencode-session всегда вычисляем сами (LCP по истории).
|
||||
val forwardedHeaders = headersToForward(call.request.headers)
|
||||
.filterKeys { sessionHeader == null || !it.equals(sessionHeader, ignoreCase = true) }
|
||||
val sessionId = sessionHeader?.let { sessions.resolve(sessionPrefixHashes(forwarded)) }
|
||||
val outgoingHeaders = forwardedHeaders.toMutableMap().apply {
|
||||
this["Content-Type"] = listOf("application/json")
|
||||
if (providerKey.isNotEmpty()) this["Authorization"] = listOf("Bearer $providerKey")
|
||||
if (sessionHeader != null && sessionId != null) this[sessionHeader] = listOf(sessionId)
|
||||
}
|
||||
log.info {
|
||||
"[llm-proxy] chat model=$modelName upstream=${up.id} → $url headers: ${formatHeadersForLog(outgoingHeaders)}"
|
||||
"[llm-proxy] chat model=$modelName upstream=${up.id} session=${sessionId ?: "-"} → $url headers: ${formatHeadersForLog(outgoingHeaders)}"
|
||||
}
|
||||
var failover = false
|
||||
var responded = false
|
||||
@@ -210,6 +219,9 @@ private suspend fun handleChat(
|
||||
set("Authorization", "Bearer $providerKey")
|
||||
}
|
||||
set("Content-Type", "application/json")
|
||||
if (sessionHeader != null && sessionId != null) {
|
||||
set(sessionHeader, sessionId)
|
||||
}
|
||||
}
|
||||
setBody(forwarded.toString())
|
||||
}.execute { resp ->
|
||||
@@ -575,6 +587,7 @@ data class ProviderConf(
|
||||
val key: String = "",
|
||||
val max_concurrency: Int? = null,
|
||||
val patch: JsonObject? = null,
|
||||
val session_header: String? = null,
|
||||
)
|
||||
|
||||
data class UpstreamConf(
|
||||
@@ -636,6 +649,7 @@ internal fun parseConfig(root: YamlElement): Config {
|
||||
key = m.strOrNull("key") ?: "",
|
||||
max_concurrency = m.strOrNull("max_concurrency")?.toIntOrNull(),
|
||||
patch = m.yamlMapOrNull("patch")?.let { yamlToJson(it) as JsonObject },
|
||||
session_header = m.strOrNull("session_header"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user