feat: codex subscription support (#6600)
Signed-off-by: Adrian Cole <adrian@tetrate.io> Co-authored-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -352,6 +352,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::config_management::remove_custom_provider,
|
||||
super::routes::config_management::check_provider,
|
||||
super::routes::config_management::set_config_provider,
|
||||
super::routes::config_management::configure_provider_oauth,
|
||||
super::routes::config_management::get_pricing,
|
||||
super::routes::prompts::get_prompts,
|
||||
super::routes::prompts::get_prompt,
|
||||
|
||||
@@ -210,6 +210,13 @@ fn mask_secret(secret: Value) -> String {
|
||||
format!("{}{}", visible, mask)
|
||||
}
|
||||
|
||||
fn is_valid_provider_name(provider_name: &str) -> bool {
|
||||
!provider_name.is_empty()
|
||||
&& provider_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/config/read",
|
||||
@@ -823,6 +830,54 @@ pub async fn set_config_provider(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/config/providers/{name}/oauth",
|
||||
params(
|
||||
("name" = String, Path, description = "Provider name")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "OAuth configuration completed"),
|
||||
(status = 400, description = "OAuth configuration failed")
|
||||
)
|
||||
)]
|
||||
pub async fn configure_provider_oauth(
|
||||
Path(provider_name): Path<String>,
|
||||
) -> Result<Json<String>, (StatusCode, String)> {
|
||||
use goose::model::ModelConfig;
|
||||
use goose::providers::create;
|
||||
|
||||
if !is_valid_provider_name(&provider_name) {
|
||||
return Err((StatusCode::BAD_REQUEST, "Invalid provider name".to_string()));
|
||||
}
|
||||
|
||||
let temp_model =
|
||||
ModelConfig::new("temp").map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
|
||||
|
||||
let provider = create(&provider_name, temp_model).await.map_err(|e| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Failed to create provider: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
provider.configure_oauth().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("OAuth configuration failed: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Mark the provider as configured after successful OAuth
|
||||
let configured_marker = format!("{}_configured", provider_name);
|
||||
let config = goose::config::Config::global();
|
||||
config
|
||||
.set_param(&configured_marker, true)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json("OAuth configuration completed".to_string()))
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/config", get(read_all_config))
|
||||
@@ -851,6 +906,10 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/config/custom-providers/{id}", get(get_custom_provider))
|
||||
.route("/config/check_provider", post(check_provider))
|
||||
.route("/config/set_provider", post(set_config_provider))
|
||||
.route(
|
||||
"/config/providers/{name}/oauth",
|
||||
post(configure_provider_oauth),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,16 @@ pub fn check_provider_configured(metadata: &ProviderMetadata, provider_type: Pro
|
||||
.is_ok();
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: OAuth providers - check for configured marker
|
||||
let has_oauth_key = metadata.config_keys.iter().any(|key| key.oauth_flow);
|
||||
if has_oauth_key {
|
||||
let configured_marker = format!("{}_configured", metadata.name);
|
||||
if matches!(config.get_param::<bool>(&configured_marker), Ok(true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: Zero-config providers (no config keys)
|
||||
if metadata.config_keys.is_empty() {
|
||||
// Check if the provider has been explicitly configured via the UI
|
||||
|
||||
@@ -77,6 +77,7 @@ rand = "0.8.5"
|
||||
utoipa = { version = "4.1", features = ["chrono"] }
|
||||
tokio-cron-scheduler = "0.14.0"
|
||||
urlencoding = "2.1"
|
||||
v_htmlescape = "0.15"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "chrono", "json"] }
|
||||
|
||||
# For Bedrock provider
|
||||
|
||||
@@ -633,9 +633,13 @@ impl McpClientTrait for AppsManagerClient {
|
||||
}
|
||||
|
||||
fn schema<T: JsonSchema>() -> JsonObject {
|
||||
serde_json::to_value(schema_for!(T))
|
||||
let mut obj = serde_json::to_value(schema_for!(T))
|
||||
.map(|v| v.as_object().unwrap().clone())
|
||||
.expect("valid schema")
|
||||
.expect("valid schema");
|
||||
// Ensure properties key exists (required by OpenAI-compatible APIs)
|
||||
obj.entry("properties")
|
||||
.or_insert_with(|| serde_json::json!({}));
|
||||
obj
|
||||
}
|
||||
|
||||
fn extract_string(args: &JsonObject, key: &str) -> Result<String, String> {
|
||||
|
||||
@@ -3724,6 +3724,10 @@
|
||||
"provider_model": "gpt-5.1-codex-mini",
|
||||
"canonical_model": "openai/gpt-5.1-codex-mini"
|
||||
},
|
||||
{
|
||||
"provider_model": "gpt-5.2-codex",
|
||||
"canonical_model": "openai/gpt-5.2-codex"
|
||||
},
|
||||
{
|
||||
"provider_model": "gpt-5.2",
|
||||
"canonical_model": "openai/gpt-5.2"
|
||||
|
||||
@@ -2033,6 +2033,27 @@
|
||||
"completion": 0.000014
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "openai/gpt-5.2-codex",
|
||||
"name": "OpenAI: GPT-5.2-Codex",
|
||||
"context_length": 400000,
|
||||
"max_completion_tokens": 128000,
|
||||
"input_modalities": [
|
||||
"file",
|
||||
"image",
|
||||
"text"
|
||||
],
|
||||
"output_modalities": [
|
||||
"text"
|
||||
],
|
||||
"supports_tools": true,
|
||||
"pricing": {
|
||||
"prompt": 1.75e-6,
|
||||
"completion": 0.000014,
|
||||
"request": 0.0,
|
||||
"image": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "openai/gpt-5.2-pro",
|
||||
"name": "OpenAI: GPT-5.2 Pro",
|
||||
|
||||
@@ -133,7 +133,10 @@ fn swap_claude_word_order(model: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
fn is_hosting_provider(provider: &str) -> bool {
|
||||
matches!(provider, "databricks" | "openrouter" | "azure" | "bedrock")
|
||||
matches!(
|
||||
provider,
|
||||
"databricks" | "openrouter" | "azure" | "bedrock" | "chatgpt_codex"
|
||||
)
|
||||
}
|
||||
|
||||
/// Infer the real provider from model name patterns
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ use super::{
|
||||
azure::AzureProvider,
|
||||
base::{Provider, ProviderMetadata},
|
||||
bedrock::BedrockProvider,
|
||||
chatgpt_codex::ChatGptCodexProvider,
|
||||
claude_code::ClaudeCodeProvider,
|
||||
codex::CodexProvider,
|
||||
cursor_agent::CursorAgentProvider,
|
||||
@@ -46,6 +47,10 @@ async fn init_registry() -> RwLock<ProviderRegistry> {
|
||||
.register::<AnthropicProvider, _>(|m| Box::pin(AnthropicProvider::from_env(m)), true);
|
||||
registry.register::<AzureProvider, _>(|m| Box::pin(AzureProvider::from_env(m)), false);
|
||||
registry.register::<BedrockProvider, _>(|m| Box::pin(BedrockProvider::from_env(m)), false);
|
||||
registry.register::<ChatGptCodexProvider, _>(
|
||||
|m| Box::pin(ChatGptCodexProvider::from_env(m)),
|
||||
true,
|
||||
);
|
||||
registry
|
||||
.register::<ClaudeCodeProvider, _>(|m| Box::pin(ClaudeCodeProvider::from_env(m)), true);
|
||||
registry.register::<CodexProvider, _>(|m| Box::pin(CodexProvider::from_env(m)), true);
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod azureauth;
|
||||
pub mod base;
|
||||
pub mod bedrock;
|
||||
pub mod canonical;
|
||||
pub mod chatgpt_codex;
|
||||
pub mod claude_code;
|
||||
pub mod codex;
|
||||
pub mod cursor_agent;
|
||||
|
||||
Reference in New Issue
Block a user