mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix: negotiate model discovery and login transition
Return the Codex catalog only for client_version requests while preserving the standard OpenAI-compatible model list for other clients. Keep the login action pending until successful route loading completes. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -2,11 +2,12 @@
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
extract::{Path, Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use gateway_core::routing::{PublicModelId, PublicModelProfile};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::ApiState;
|
||||
@@ -19,22 +20,30 @@ use super::{
|
||||
const MODEL_CREATED_TIMESTAMP: i64 = 1_700_000_000;
|
||||
const CODEX_BASE_INSTRUCTIONS: &str = "You are Codex, a coding agent. Follow the user's instructions and use the available tools to complete software engineering tasks. Inspect relevant files before editing, preserve unrelated changes, and verify the result.";
|
||||
|
||||
/// `GET /v1/models`。
|
||||
pub(crate) async fn models(State(state): State<ApiState>, headers: HeaderMap) -> Response {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct ModelsQuery {
|
||||
client_version: Option<String>,
|
||||
}
|
||||
|
||||
/// `GET /v1/models`。Codex 携带 `client_version` 时返回其专用目录合同。
|
||||
pub(crate) async fn models(
|
||||
State(state): State<ApiState>,
|
||||
Query(query): Query<ModelsQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
let service = state.openai();
|
||||
let client = match authenticate_client(service, &headers) {
|
||||
Ok(client) => client,
|
||||
Err(error) => return authentication_error_response(error),
|
||||
};
|
||||
let data = service
|
||||
.public_models(&client)
|
||||
.into_iter()
|
||||
.map(|model| openai_model_json(&model))
|
||||
.collect::<Vec<_>>();
|
||||
let profiles = service.public_model_profiles(&client);
|
||||
|
||||
if !profiles.is_empty() {
|
||||
let models = profiles
|
||||
if query
|
||||
.client_version
|
||||
.as_deref()
|
||||
.is_some_and(|version| !version.trim().is_empty())
|
||||
{
|
||||
let models = service
|
||||
.public_model_profiles(&client)
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, profile)| codex_model_json(profile, index))
|
||||
@@ -42,14 +51,17 @@ pub(crate) async fn models(State(state): State<ApiState>, headers: HeaderMap) ->
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"object": "list",
|
||||
"data": data,
|
||||
"models": models,
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let data = service
|
||||
.public_models(&client)
|
||||
.into_iter()
|
||||
.map(|model| openai_model_json(&model))
|
||||
.collect::<Vec<_>>();
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
|
||||
@@ -135,6 +135,12 @@ async fn models_should_encode_provider_profiles_for_current_codex_clients() {
|
||||
let value: serde_json::Value = serde_json::from_slice(&body).expect("models JSON");
|
||||
let model = &value["models"][0];
|
||||
|
||||
assert_eq!(
|
||||
value
|
||||
.as_object()
|
||||
.map(|catalog| catalog.keys().map(String::as_str).collect::<Vec<_>>()),
|
||||
Some(vec!["models"])
|
||||
);
|
||||
assert_eq!(model["slug"], "grok-4.5");
|
||||
assert_eq!(model["default_reasoning_level"], "medium");
|
||||
assert_eq!(model["context_window"], 500_000);
|
||||
@@ -163,6 +169,21 @@ async fn models_should_encode_provider_profiles_for_current_codex_clients() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn models_should_keep_the_codex_contract_when_profiles_are_empty() {
|
||||
let response = api_router(ModelsExecution::new())
|
||||
.await
|
||||
.oneshot(authorized_request("/v1/models?client_version=0.145.0"))
|
||||
.await
|
||||
.expect("list empty Codex models response");
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("read empty Codex models body");
|
||||
let value: serde_json::Value = serde_json::from_slice(&body).expect("Codex models JSON");
|
||||
|
||||
assert_eq!(value, serde_json::json!({ "models": [] }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn models_with_provider_profiles_should_keep_the_openai_list_contract() {
|
||||
let response = api_router(ModelsExecution::with_profiles())
|
||||
@@ -176,10 +197,7 @@ async fn models_with_provider_profiles_should_keep_the_openai_list_contract() {
|
||||
let value: serde_json::Value = serde_json::from_slice(&body).expect("models JSON");
|
||||
|
||||
assert_eq!(
|
||||
serde_json::json!({
|
||||
"object": value["object"],
|
||||
"data": value["data"],
|
||||
}),
|
||||
value,
|
||||
serde_json::json!({
|
||||
"object": "list",
|
||||
"data": [
|
||||
|
||||
@@ -17,21 +17,35 @@ const { toggleTheme } = uiStore
|
||||
|
||||
const username = shallowRef('')
|
||||
const password = shallowRef('')
|
||||
const loginPending = shallowRef(false)
|
||||
const canSubmit = computed<boolean>(() => !!username.value.trim() && !!password.value.trim())
|
||||
const submitDisabled = computed<boolean>(() => authStore.loading || !canSubmit.value)
|
||||
const loginLoading = computed<boolean>(() => authStore.loading || loginPending.value)
|
||||
const submitDisabled = computed<boolean>(() => loginLoading.value || !canSubmit.value)
|
||||
|
||||
async function handleSubmit(): Promise<void> {
|
||||
if (!canSubmit.value) {
|
||||
if (!canSubmit.value || loginPending.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loginPending.value = true
|
||||
const success = await authStore.login({
|
||||
username: username.value.trim(),
|
||||
password: password.value,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
router.push('/')
|
||||
if (!success) {
|
||||
loginPending.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await router.push('/')
|
||||
}
|
||||
finally {
|
||||
// 成功时登录页会卸载;导航失败并停留当前页时才恢复按钮。
|
||||
if (router.currentRoute.value.path === '/login') {
|
||||
loginPending.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -48,7 +62,7 @@ async function handleSubmit(): Promise<void> {
|
||||
v-model:username="username"
|
||||
v-model:password="password"
|
||||
:error="authStore.error"
|
||||
:loading="authStore.loading"
|
||||
:loading="loginLoading"
|
||||
:submit-disabled="submitDisabled"
|
||||
:effective-theme="effectiveTheme"
|
||||
@submit="handleSubmit"
|
||||
|
||||
Reference in New Issue
Block a user