migrate ai format conversion to responses adapters

This commit is contained in:
fawney19
2026-04-26 20:32:55 +08:00
parent e36fb8c07a
commit 5b914aa78c
183 changed files with 13539 additions and 3401 deletions

View File

@@ -7,7 +7,11 @@ use regex::Regex;
use serde_json::{json, Value};
const MODEL_FETCH_FORMAT_PRIORITY: &[&[&str]] = &[
&["openai:chat", "openai:cli", "openai:compact"],
&[
"openai:chat",
"openai:responses",
"openai:responses:compact",
],
&["claude:chat", "claude:cli"],
&["gemini:chat", "gemini:cli"],
];
@@ -180,9 +184,15 @@ pub fn selected_models_fetch_endpoints(
if !key_formats.is_empty() && !key_formats.contains(&api_format) {
continue;
}
by_format
.entry(api_format)
.or_insert_with(|| endpoint.clone());
if let Some(existing) = by_format.get_mut(&api_format) {
if endpoint.api_format.trim().eq_ignore_ascii_case(&api_format)
&& !existing.api_format.trim().eq_ignore_ascii_case(&api_format)
{
*existing = endpoint.clone();
}
} else {
by_format.insert(api_format, endpoint.clone());
}
}
MODEL_FETCH_FORMAT_PRIORITY
@@ -209,8 +219,8 @@ pub fn endpoint_supports_rust_models_fetch(api_format: &str) -> bool {
matches!(
api_format.as_str(),
"openai:chat"
| "openai:cli"
| "openai:compact"
| "openai:responses"
| "openai:responses:compact"
| "claude:chat"
| "claude:cli"
| "gemini:chat"
@@ -250,7 +260,7 @@ pub fn preset_models_for_provider(provider_type: &str) -> Option<Vec<Value>> {
preset_model("claude-haiku-4-5-20251001", "anthropic", "Claude Haiku 4.5", "claude:cli"),
],
"codex" => vec![
preset_model("gpt-5", "openai", "GPT-5", "openai:cli"),
preset_model("gpt-5", "openai", "GPT-5", "openai:responses"),
preset_model("gpt-image-1", "openai", "GPT Image 1", "openai:image"),
preset_model("gpt-image-1.5", "openai", "GPT Image 1.5", "openai:image"),
preset_model("gpt-image-1-mini", "openai", "GPT Image 1 Mini", "openai:image"),
@@ -258,16 +268,26 @@ pub fn preset_models_for_provider(provider_type: &str) -> Option<Vec<Value>> {
preset_model("chatgpt-image-latest", "openai", "ChatGPT Image Latest", "openai:image"),
preset_model("dall-e-2", "openai", "DALL-E 2", "openai:image"),
preset_model("dall-e-3", "openai", "DALL-E 3", "openai:image"),
preset_model("gpt-5-codex", "openai", "GPT-5 Codex", "openai:cli"),
preset_model("gpt-5-codex-mini", "openai", "GPT-5 Codex Mini", "openai:cli"),
preset_model("gpt-5.1", "openai", "GPT-5.1", "openai:cli"),
preset_model("gpt-5.1-codex", "openai", "GPT-5.1 Codex", "openai:cli"),
preset_model("gpt-5.1-codex-mini", "openai", "GPT-5.1 Codex Mini", "openai:cli"),
preset_model("gpt-5.1-codex-max", "openai", "GPT-5.1 Codex Max", "openai:cli"),
preset_model("gpt-5.2", "openai", "GPT-5.2", "openai:cli"),
preset_model("gpt-5.2-codex", "openai", "GPT-5.2 Codex", "openai:cli"),
preset_model("gpt-5.3-codex", "openai", "GPT-5.3 Codex", "openai:cli"),
preset_model("gpt-5.4", "openai", "GPT-5.4", "openai:cli"),
preset_model("gpt-5-codex", "openai", "GPT-5 Codex", "openai:responses"),
preset_model("gpt-5-codex-mini", "openai", "GPT-5 Codex Mini", "openai:responses"),
preset_model("gpt-5.1", "openai", "GPT-5.1", "openai:responses"),
preset_model("gpt-5.1-codex", "openai", "GPT-5.1 Codex", "openai:responses"),
preset_model(
"gpt-5.1-codex-mini",
"openai",
"GPT-5.1 Codex Mini",
"openai:responses",
),
preset_model(
"gpt-5.1-codex-max",
"openai",
"GPT-5.1 Codex Max",
"openai:responses",
),
preset_model("gpt-5.2", "openai", "GPT-5.2", "openai:responses"),
preset_model("gpt-5.2-codex", "openai", "GPT-5.2 Codex", "openai:responses"),
preset_model("gpt-5.3-codex", "openai", "GPT-5.3 Codex", "openai:responses"),
preset_model("gpt-5.4", "openai", "GPT-5.4", "openai:responses"),
],
_ => return None,
};
@@ -550,7 +570,11 @@ fn wildcard_matches(pattern: &str, model_id: &str) -> bool {
}
fn normalize_api_format(value: &str) -> String {
value.trim().to_ascii_lowercase()
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
}
#[cfg(test)]
@@ -644,7 +668,7 @@ mod tests {
fn aggregate_models_for_cache_merges_api_formats_and_sorts_by_model_id() {
let aggregated = aggregate_models_for_cache(&[
json!({"id":"zeta","api_formats":["openai:chat"]}),
json!({"id":"alpha","api_formats":["openai:cli"]}),
json!({"id":"alpha","api_formats":["openai:responses"]}),
json!({"id":"alpha","api_formats":["openai:chat"]}),
]);
assert_eq!(aggregated.len(), 2);
@@ -652,7 +676,7 @@ mod tests {
assert_eq!(aggregated[1]["id"], "zeta");
assert_eq!(
aggregated[0]["api_formats"],
json!(["openai:chat", "openai:cli"])
json!(["openai:chat", "openai:responses"])
);
}
@@ -679,10 +703,13 @@ mod tests {
}
#[test]
fn build_models_fetch_url_excludes_openai_responses() {
fn build_models_fetch_url_supports_openai_responses() {
assert_eq!(
build_models_fetch_url("openai", "openai:responses", "https://example.com"),
None
Some((
"https://example.com/v1/models".to_string(),
"openai:responses".to_string()
))
);
}
@@ -716,7 +743,7 @@ mod tests {
}
#[test]
fn selected_models_fetch_endpoints_prefers_chat_and_excludes_responses() {
fn selected_models_fetch_endpoints_prefers_chat_then_responses() {
let key = sample_key("provider-1", "key-1", &["openai:chat", "openai:responses"]);
let endpoints = vec![
sample_endpoint(
@@ -741,6 +768,25 @@ mod tests {
let selected = selected_models_fetch_endpoints(&endpoints, &key);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].id, "endpoint-chat");
let key = sample_key("provider-1", "key-1", &["openai:responses"]);
let endpoints = vec![
sample_endpoint(
"provider-1",
"endpoint-cli",
"openai:cli",
"https://example.com",
),
sample_endpoint(
"provider-1",
"endpoint-responses",
"openai:responses",
"https://example.com",
),
];
let selected = selected_models_fetch_endpoints(&endpoints, &key);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].id, "endpoint-responses");
}
#[test]

View File

@@ -19,7 +19,7 @@ use serde_json::json;
use crate::build_models_fetch_url;
const OPENAI_CLI_USER_AGENT: &str = "openai-codex/1.0";
const OPENAI_RESPONSES_USER_AGENT: &str = "openai-codex/1.0";
const CLAUDE_CLI_USER_AGENT: &str = "claude-code/1.0.1";
const GEMINI_CLI_USER_AGENT: &str = "GeminiCLI/0.1.5 (Windows; AMD64)";
const CLAUDE_VERSION_HEADER: &str = "2023-06-01";
@@ -383,8 +383,11 @@ fn apply_fetch_header_rules(
fn standard_models_fetch_headers(api_format: &str) -> BTreeMap<String, String> {
let api_format = api_format.trim().to_ascii_lowercase();
match api_format.as_str() {
"openai:cli" | "openai:compact" => {
BTreeMap::from([("user-agent".to_string(), OPENAI_CLI_USER_AGENT.to_string())])
"openai:responses" | "openai:responses:compact" | "openai:cli" | "openai:compact" => {
BTreeMap::from([(
"user-agent".to_string(),
OPENAI_RESPONSES_USER_AGENT.to_string(),
)])
}
"claude:chat" => BTreeMap::from([(
"anthropic-version".to_string(),
@@ -577,12 +580,12 @@ mod tests {
}
#[tokio::test]
async fn builds_openai_cli_models_fetch_plan_with_cli_user_agent() {
async fn builds_openai_responses_models_fetch_plan_with_codex_user_agent() {
let runtime = TestRuntime {
oauth_auth: None,
proxy: None,
};
let mut transport = sample_transport("openai", "openai:cli", "api_key");
let mut transport = sample_transport("openai", "openai:responses", "api_key");
transport.key.decrypted_auth_config = None;
let plan = build_models_fetch_execution_plan(&runtime, &transport)
.await
@@ -600,12 +603,12 @@ mod tests {
}
#[tokio::test]
async fn builds_openai_compact_models_fetch_plan_with_bearer_authorization() {
async fn builds_openai_responses_compact_models_fetch_plan_with_bearer_authorization() {
let runtime = TestRuntime {
oauth_auth: None,
proxy: None,
};
let mut transport = sample_transport("openai", "openai:compact", "api_key");
let mut transport = sample_transport("openai", "openai:responses:compact", "api_key");
transport.key.decrypted_auth_config = None;
let plan = build_models_fetch_execution_plan(&runtime, &transport)
.await