Skip to content

Commit d7d0c8d

Browse files
authored
Merge pull request #1033 from openabdev/fix/openab-agent-static-codex-models
fix(openab-agent): use static model lists for /models command
2 parents ed2225c + 3780ff2 commit d7d0c8d

1 file changed

Lines changed: 31 additions & 133 deletions

File tree

openab-agent/src/acp.rs

Lines changed: 31 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ use serde::{Deserialize, Serialize};
44
use serde_json::{json, Value};
55
use std::collections::HashMap;
66
use std::io::{self, BufRead, Write};
7-
use std::time::Duration;
87
use tokio::sync::mpsc;
98
use uuid::Uuid;
109

11-
const MODEL_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
12-
1310
#[derive(Debug, Deserialize)]
1411
pub struct JsonRpcRequest {
1512
pub id: Option<u64>,
@@ -235,7 +232,7 @@ impl AcpServer {
235232
if active_provider == "anthropic" {
236233
"claude-sonnet-4-20250514".to_string()
237234
} else {
238-
"gpt-4.1-nano".to_string()
235+
"gpt-5.4-mini".to_string()
239236
}
240237
});
241238
self.model_options = Self::available_models().await;
@@ -260,135 +257,18 @@ impl AcpServer {
260257
}
261258

262259
/// List available models based on configured credentials.
263-
/// Queries provider APIs when possible, falls back to known defaults.
260+
/// Uses static model lists (same approach as Pi coding agent).
264261
async fn available_models() -> Vec<ModelOption> {
265-
let has_anthropic = std::env::var("ANTHROPIC_API_KEY").is_ok();
266-
let has_openai = crate::auth::load_tokens().is_ok();
267-
268-
let (anthropic_models, openai_models) = tokio::join!(
269-
async {
270-
if has_anthropic {
271-
Self::fetch_anthropic_models()
272-
.await
273-
.unwrap_or_else(|_| Self::fallback_anthropic_models())
274-
} else {
275-
Vec::new()
276-
}
277-
},
278-
async {
279-
if has_openai {
280-
Self::fetch_openai_models()
281-
.await
282-
.unwrap_or_else(|_| Self::fallback_openai_models())
283-
} else {
284-
Vec::new()
285-
}
286-
},
287-
);
288-
289-
let mut models = anthropic_models;
290-
models.extend(openai_models);
291-
292-
if models.is_empty() {
293-
models.push(ModelOption::new(
294-
"none",
295-
"No credentials configured",
296-
"none",
297-
));
298-
}
299-
models
300-
}
301-
302-
/// Fetch models from Anthropic /v1/models API.
303-
async fn fetch_anthropic_models() -> Result<Vec<ModelOption>, String> {
304-
let api_key = std::env::var("ANTHROPIC_API_KEY").map_err(|e| e.to_string())?;
305-
let client = reqwest::Client::builder()
306-
.timeout(MODEL_DISCOVERY_TIMEOUT)
307-
.build()
308-
.map_err(|e| e.to_string())?;
309-
let resp = client
310-
.get("https://api.anthropic.com/v1/models")
311-
.header("x-api-key", &api_key)
312-
.header("anthropic-version", "2023-06-01")
313-
.send()
314-
.await
315-
.map_err(|e| e.to_string())?;
316-
317-
if !resp.status().is_success() {
318-
return Err(format!("Anthropic API returned {}", resp.status()));
319-
}
320-
321-
let body: Value = resp.json().await.map_err(|e| e.to_string())?;
322-
let mut models = Vec::new();
323-
if let Some(data) = body.get("data").and_then(|d| d.as_array()) {
324-
for m in data {
325-
if let Some(id) = m.get("id").and_then(|v| v.as_str()) {
326-
// Only include chat models (claude-*)
327-
if id.starts_with("claude") {
328-
let display = m.get("display_name").and_then(|v| v.as_str()).unwrap_or(id);
329-
models.push(ModelOption::new(id, display, "anthropic"));
330-
}
331-
}
332-
}
333-
}
334-
if models.is_empty() {
335-
return Err("No models returned from Anthropic API".to_string());
336-
}
337-
Ok(models)
262+
Self::static_available_models()
338263
}
339264

340-
/// Fetch models from OpenAI-compatible /models endpoint.
341-
async fn fetch_openai_models() -> Result<Vec<ModelOption>, String> {
342-
let tokens = crate::auth::load_tokens().map_err(|e| e.to_string())?;
343-
let base_url = std::env::var("OPENAB_AGENT_OPENAI_BASE_URL")
344-
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string());
345-
let client = reqwest::Client::builder()
346-
.timeout(MODEL_DISCOVERY_TIMEOUT)
347-
.build()
348-
.map_err(|e| e.to_string())?;
349-
let resp = client
350-
.get(format!("{}/models", base_url))
351-
.bearer_auth(&tokens.access_token)
352-
.send()
353-
.await
354-
.map_err(|e| e.to_string())?;
355-
356-
if !resp.status().is_success() {
357-
return Err(format!("OpenAI API returned {}", resp.status()));
358-
}
359-
360-
let body: Value = resp.json().await.map_err(|e| e.to_string())?;
361-
let mut models = Vec::new();
362-
// Handle both { "data": [...] } and [...] shapes
363-
let items = body
364-
.get("data")
365-
.and_then(|d| d.as_array())
366-
.or_else(|| body.as_array());
367-
if let Some(data) = items {
368-
for m in data {
369-
if let Some(id) = m.get("id").and_then(|v| v.as_str()) {
370-
let name = m
371-
.get("name")
372-
.or_else(|| m.get("id"))
373-
.and_then(|v| v.as_str())
374-
.unwrap_or(id);
375-
models.push(ModelOption::new(id, name, "openai"));
376-
}
377-
}
378-
}
379-
if models.is_empty() {
380-
return Err("No models returned from OpenAI API".to_string());
381-
}
382-
Ok(models)
383-
}
384-
385-
fn fallback_available_models() -> Vec<ModelOption> {
265+
fn static_available_models() -> Vec<ModelOption> {
386266
let mut models = Vec::new();
387267
if std::env::var("ANTHROPIC_API_KEY").is_ok() {
388-
models.extend(Self::fallback_anthropic_models());
268+
models.extend(Self::static_anthropic_models());
389269
}
390270
if crate::auth::load_tokens().is_ok() {
391-
models.extend(Self::fallback_openai_models());
271+
models.extend(Self::static_openai_models());
392272
}
393273
if models.is_empty() {
394274
models.push(ModelOption::new(
@@ -400,18 +280,36 @@ impl AcpServer {
400280
models
401281
}
402282

403-
fn fallback_anthropic_models() -> Vec<ModelOption> {
283+
fn static_anthropic_models() -> Vec<ModelOption> {
284+
// From models.dev/api.json — Anthropic models with tool_call support.
285+
// Dated versions used for deterministic pinning.
404286
vec![
287+
ModelOption::new("claude-haiku-4-5-20251001", "Claude Haiku 4.5", "anthropic"),
405288
ModelOption::new("claude-sonnet-4-20250514", "Claude Sonnet 4", "anthropic"),
406-
ModelOption::new("claude-haiku-4-20250514", "Claude Haiku 4", "anthropic"),
289+
ModelOption::new(
290+
"claude-sonnet-4-5-20250929",
291+
"Claude Sonnet 4.5",
292+
"anthropic",
293+
),
294+
ModelOption::new("claude-sonnet-4-6", "Claude Sonnet 4.6", "anthropic"),
295+
ModelOption::new("claude-opus-4-20250514", "Claude Opus 4", "anthropic"),
296+
ModelOption::new("claude-opus-4-1-20250805", "Claude Opus 4.1", "anthropic"),
297+
ModelOption::new("claude-opus-4-5-20251101", "Claude Opus 4.5", "anthropic"),
298+
ModelOption::new("claude-opus-4-6", "Claude Opus 4.6", "anthropic"),
299+
ModelOption::new("claude-opus-4-7", "Claude Opus 4.7", "anthropic"),
300+
ModelOption::new("claude-opus-4-8", "Claude Opus 4.8", "anthropic"),
407301
]
408302
}
409303

410-
fn fallback_openai_models() -> Vec<ModelOption> {
304+
fn static_openai_models() -> Vec<ModelOption> {
305+
// Static list matching Pi's openai-codex provider models.
306+
// chatgpt.com/backend-api/models does not support standard model listing,
307+
// so we maintain this list explicitly (same approach as Pi coding agent).
411308
vec![
412-
ModelOption::new("gpt-4.1-nano", "GPT-4.1 Nano", "openai"),
413-
ModelOption::new("gpt-4.1-mini", "GPT-4.1 Mini", "openai"),
414-
ModelOption::new("o4-mini", "o4-mini", "openai"),
309+
ModelOption::new("gpt-5.3-codex-spark", "GPT-5.3 Codex Spark", "openai"),
310+
ModelOption::new("gpt-5.4", "GPT-5.4", "openai"),
311+
ModelOption::new("gpt-5.4-mini", "GPT-5.4 mini", "openai"),
312+
ModelOption::new("gpt-5.5", "GPT-5.5", "openai"),
415313
]
416314
}
417315

@@ -487,7 +385,7 @@ impl AcpServer {
487385
}
488386

489387
let models = if self.model_options.is_empty() {
490-
Self::fallback_available_models()
388+
Self::static_available_models()
491389
} else {
492390
self.model_options.clone()
493391
};

0 commit comments

Comments
 (0)