mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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:
Generated
+7
@@ -3050,6 +3050,7 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
"v_htmlescape",
|
||||
"webbrowser",
|
||||
"which 8.0.0",
|
||||
"winapi",
|
||||
@@ -8584,6 +8585,12 @@ dependencies = [
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "v_htmlescape"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
crates/goose-bench/src/eval_suites/core/developer/simple_repo_clone_test.rs::run
|
||||
crates/goose-cli/src/cli.rs::cli
|
||||
crates/goose-cli/src/commands/configure.rs::configure_extensions_dialog
|
||||
crates/goose-cli/src/commands/configure.rs::configure_provider_dialog
|
||||
crates/goose-cli/src/commands/configure.rs::configure_tool_permissions_dialog
|
||||
crates/goose-cli/src/commands/configure.rs::handle_configure
|
||||
crates/goose-cli/src/commands/project.rs::handle_project_default
|
||||
crates/goose-cli/src/commands/project.rs::handle_projects_interactive
|
||||
crates/goose-cli/src/commands/web.rs::process_message_streaming
|
||||
crates/goose-cli/src/session/builder.rs::build_session
|
||||
crates/goose-cli/src/session/export.rs::tool_response_to_markdown
|
||||
crates/goose-cli/src/session/mod.rs::interactive
|
||||
crates/goose-cli/src/session/mod.rs::process_agent_response
|
||||
crates/goose-mcp/src/computercontroller/docx_tool.rs::docx_tool
|
||||
crates/goose-mcp/src/computercontroller/mod.rs::new
|
||||
crates/goose-mcp/src/computercontroller/pdf_tool.rs::pdf_tool
|
||||
crates/goose-mcp/src/memory/mod.rs::new
|
||||
@@ -23,12 +16,10 @@ crates/goose/src/agents/agent.rs::create_recipe
|
||||
crates/goose/src/agents/agent.rs::dispatch_tool_call
|
||||
crates/goose/src/agents/agent.rs::reply
|
||||
crates/goose/src/agents/agent.rs::reply_internal
|
||||
crates/goose/src/agents/extension_manager.rs::add_extension
|
||||
crates/goose/src/providers/formats/anthropic.rs::format_messages
|
||||
crates/goose/src/providers/formats/anthropic.rs::response_to_streaming_message<S>
|
||||
crates/goose/src/providers/formats/anthropic.rs::response_to_streaming_message
|
||||
crates/goose/src/providers/formats/databricks.rs::format_messages
|
||||
crates/goose/src/providers/formats/google.rs::format_messages
|
||||
crates/goose/src/providers/formats/openai.rs::format_messages
|
||||
crates/goose/src/providers/formats/openai.rs::response_to_streaming_message<S>
|
||||
crates/goose/src/providers/gcpvertexai.rs::post_with_location
|
||||
crates/goose/src/providers/formats/openai.rs::response_to_streaming_message
|
||||
crates/goose/src/providers/snowflake.rs::post
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,7 +27,9 @@ parse_violation() {
|
||||
case "$violation_parser" in
|
||||
"function_name")
|
||||
jq -r 'select(.message.code.code == "'"$rule_code"'") |
|
||||
"\(.message.spans[0].file_name)::\(.message.spans[0].text[0].text | split("fn ")[1] | split("(")[0])"'
|
||||
.message.spans[0] as $span |
|
||||
($span.text | map(.text) | map(select(test("\\bfn\\b"))) | first // "") as $line |
|
||||
if $line == "" then empty else "\($span.file_name)::\($line | capture("fn\\s+(?<name>[a-z_][a-z0-9_]*)") | .name)" end'
|
||||
;;
|
||||
"type_name")
|
||||
jq -r 'select(.message.code.code == "'"$rule_code"'") |
|
||||
|
||||
@@ -1325,6 +1325,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/providers/{name}/oauth": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "configure_provider_oauth",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Provider name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OAuth configuration completed"
|
||||
},
|
||||
"400": {
|
||||
"description": "OAuth configuration failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/read": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2261,6 +2261,32 @@ export type GetProviderModelsResponses = {
|
||||
|
||||
export type GetProviderModelsResponse = GetProviderModelsResponses[keyof GetProviderModelsResponses];
|
||||
|
||||
export type ConfigureProviderOauthData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Provider name
|
||||
*/
|
||||
name: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/config/providers/{name}/oauth';
|
||||
};
|
||||
|
||||
export type ConfigureProviderOauthErrors = {
|
||||
/**
|
||||
* OAuth configuration failed
|
||||
*/
|
||||
400: unknown;
|
||||
};
|
||||
|
||||
export type ConfigureProviderOauthResponses = {
|
||||
/**
|
||||
* OAuth configuration completed
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type ReadConfigData = {
|
||||
body: ConfigKeyQuery;
|
||||
path?: never;
|
||||
|
||||
@@ -73,20 +73,19 @@ export default function ApiKeyTester({ onSuccess, onStartTesting }: ApiKeyTester
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full p-4 sm:p-6 bg-background-muted border border-background-hover rounded-xl">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<Key className="w-4 h-4 mb-3 text-text-standard" />
|
||||
<div className="w-full p-3 sm:p-4 bg-background-muted border border-background-hover rounded-xl">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Key className="w-4 h-4 text-text-standard flex-shrink-0" />
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
|
||||
<h3 className="font-medium text-text-standard text-sm sm:text-base">
|
||||
Quick Setup with API Key
|
||||
</h3>
|
||||
<span className="text-text-muted text-xs sm:text-sm">
|
||||
Auto-detect your provider
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-text-muted text-sm sm:text-base mb-4">
|
||||
Enter your API key and we'll automatically detect which provider it works with.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 items-stretch">
|
||||
<input
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useConfig } from './ConfigContext';
|
||||
import { SetupModal } from './SetupModal';
|
||||
import { startOpenRouterSetup } from '../utils/openRouterSetup';
|
||||
import { startTetrateSetup } from '../utils/tetrateSetup';
|
||||
import { startChatGptCodexSetup } from '../utils/chatgptCodexSetup';
|
||||
import WelcomeGooseLogo from './WelcomeGooseLogo';
|
||||
import { toastService } from '../toasts';
|
||||
import { OllamaSetup } from './OllamaSetup';
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
trackOnboardingSetupFailed,
|
||||
} from '../utils/analytics';
|
||||
|
||||
import { Goose, OpenRouter, Tetrate } from './icons';
|
||||
import { Goose, OpenRouter, Tetrate, ChatGPT } from './icons';
|
||||
|
||||
interface ProviderGuardProps {
|
||||
didSelectProvider: boolean;
|
||||
@@ -69,6 +70,14 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
|
||||
autoClose?: number;
|
||||
} | null>(null);
|
||||
|
||||
const [chatgptCodexSetupState, setChatgptCodexSetupState] = useState<{
|
||||
show: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
showRetry: boolean;
|
||||
autoClose?: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleTetrateSetup = async () => {
|
||||
trackOnboardingProviderSelected('tetrate');
|
||||
try {
|
||||
@@ -97,6 +106,34 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
|
||||
}
|
||||
};
|
||||
|
||||
const handleChatGptCodexSetup = async () => {
|
||||
trackOnboardingProviderSelected('chatgpt_codex');
|
||||
try {
|
||||
const result = await startChatGptCodexSetup();
|
||||
if (result.success) {
|
||||
setSwitchModelProvider('chatgpt_codex');
|
||||
setShowSwitchModelModal(true);
|
||||
} else {
|
||||
trackOnboardingSetupFailed('chatgpt_codex', result.message);
|
||||
setChatgptCodexSetupState({
|
||||
show: true,
|
||||
title: 'Setup Failed',
|
||||
message: result.message,
|
||||
showRetry: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ChatGPT Codex setup error:', error);
|
||||
trackOnboardingSetupFailed('chatgpt_codex', 'unexpected_error');
|
||||
setChatgptCodexSetupState({
|
||||
show: true,
|
||||
title: 'Setup Error',
|
||||
message: 'An unexpected error occurred during setup.',
|
||||
showRetry: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleApiKeySuccess = async (provider: string, _model: string, apiKey: string) => {
|
||||
trackOnboardingProviderSelected('api_key');
|
||||
const keyName = `${provider.toUpperCase()}_API_KEY`;
|
||||
@@ -163,21 +200,26 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
|
||||
setShowOllamaSetup(false);
|
||||
};
|
||||
|
||||
const handleRetrySetup = (setupType: 'openrouter' | 'tetrate') => {
|
||||
const handleRetrySetup = (setupType: 'openrouter' | 'tetrate' | 'chatgpt_codex') => {
|
||||
if (setupType === 'openrouter') {
|
||||
setOpenRouterSetupState(null);
|
||||
handleOpenRouterSetup();
|
||||
} else {
|
||||
} else if (setupType === 'tetrate') {
|
||||
setTetrateSetupState(null);
|
||||
handleTetrateSetup();
|
||||
} else {
|
||||
setChatgptCodexSetupState(null);
|
||||
handleChatGptCodexSetup();
|
||||
}
|
||||
};
|
||||
|
||||
const closeSetupModal = (setupType: 'openrouter' | 'tetrate') => {
|
||||
const closeSetupModal = (setupType: 'openrouter' | 'tetrate' | 'chatgpt_codex') => {
|
||||
if (setupType === 'openrouter') {
|
||||
setOpenRouterSetupState(null);
|
||||
} else {
|
||||
} else if (setupType === 'tetrate') {
|
||||
setTetrateSetupState(null);
|
||||
} else {
|
||||
setChatgptCodexSetupState(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -274,9 +316,49 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ChatGPT Subscription Card - Full Width */}
|
||||
<div className="relative w-full mb-4">
|
||||
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-600 text-white rounded-full">
|
||||
Recommended if you have ChatGPT Plus/Pro
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={handleChatGptCodexSetup}
|
||||
className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ChatGPT className="w-5 h-5 text-text-standard" />
|
||||
<span className="font-medium text-text-standard text-sm sm:text-base">
|
||||
ChatGPT Subscription
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-text-muted group-hover:text-text-standard transition-colors">
|
||||
<svg
|
||||
className="w-4 h-4 sm:w-5 sm:h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-text-muted text-sm sm:text-base">
|
||||
Use your ChatGPT Plus/Pro subscription for GPT-5 Codex models.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tetrate Card - Full Width */}
|
||||
<div className="relative w-full mb-4">
|
||||
{/* Recommended pill */}
|
||||
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-600 text-white rounded-full">
|
||||
Recommended for new users
|
||||
@@ -415,6 +497,17 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
|
||||
/>
|
||||
)}
|
||||
|
||||
{chatgptCodexSetupState?.show && (
|
||||
<SetupModal
|
||||
title={chatgptCodexSetupState.title}
|
||||
message={chatgptCodexSetupState.message}
|
||||
showRetry={chatgptCodexSetupState.showRetry}
|
||||
onRetry={() => handleRetrySetup('chatgpt_codex')}
|
||||
onClose={() => closeSetupModal('chatgpt_codex')}
|
||||
autoClose={chatgptCodexSetupState.autoClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showSwitchModelModal && (
|
||||
<SwitchModelModal
|
||||
sessionId={null}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export default function ChatGPT({ className = '' }) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import ArrowDown from './ArrowDown';
|
||||
import ArrowUp from './ArrowUp';
|
||||
import Attach from './Attach';
|
||||
import Back from './Back';
|
||||
import ChatGPT from './ChatGPT';
|
||||
import { Bird1 } from './Bird1';
|
||||
import { Bird2 } from './Bird2';
|
||||
import { Bird3 } from './Bird3';
|
||||
@@ -59,6 +60,7 @@ export {
|
||||
ArrowUp,
|
||||
Attach,
|
||||
Back,
|
||||
ChatGPT,
|
||||
Bird1,
|
||||
Bird2,
|
||||
Bird3,
|
||||
|
||||
@@ -187,7 +187,8 @@ export const SwitchModelModal = ({
|
||||
// Load providers for manual model selection
|
||||
(async () => {
|
||||
try {
|
||||
const providersResponse = await getProviders(false);
|
||||
// Force refresh if initialProvider is set (OAuth flow needs fresh data)
|
||||
const providersResponse = await getProviders(!!initialProvider);
|
||||
const activeProviders = providersResponse.filter((provider) => provider.is_configured);
|
||||
// Create provider options and add "Use other provider" option
|
||||
setProviderOptions([
|
||||
@@ -260,7 +261,7 @@ export const SwitchModelModal = ({
|
||||
setLoadingModels(false);
|
||||
}
|
||||
})();
|
||||
}, [getProviders, getProviderModels, usePredefinedModels, read]);
|
||||
}, [getProviders, getProviderModels, usePredefinedModels, read, initialProvider]);
|
||||
|
||||
const filteredModelOptions = provider
|
||||
? modelOptions.filter((group) => group.options[0]?.provider === provider)
|
||||
|
||||
@@ -16,10 +16,24 @@ import { SecureStorageNotice } from './subcomponents/SecureStorageNotice';
|
||||
import { providerConfigSubmitHandler } from './subcomponents/handlers/DefaultSubmitHandler';
|
||||
import { useConfig } from '../../../ConfigContext';
|
||||
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { ProviderDetails, removeCustomProvider } from '../../../../api';
|
||||
import { AlertTriangle, LogIn } from 'lucide-react';
|
||||
import { ProviderDetails, removeCustomProvider, configureProviderOauth } from '../../../../api';
|
||||
import { Button } from '../../../../components/ui/button';
|
||||
|
||||
const formatErrorMessage = (error: unknown): string => {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch {
|
||||
return String(error);
|
||||
}
|
||||
};
|
||||
|
||||
interface ProviderConfigurationModalProps {
|
||||
provider: ProviderDetails;
|
||||
onClose: () => void;
|
||||
@@ -38,11 +52,15 @@ export default function ProviderConfigurationModal({
|
||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
||||
const [isActiveProvider, setIsActiveProvider] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isOAuthLoading, setIsOAuthLoading] = useState(false);
|
||||
|
||||
const requiredParameters = provider.metadata.config_keys.filter(
|
||||
(param) => param.required === true
|
||||
);
|
||||
|
||||
// Check if this provider uses OAuth for configuration
|
||||
const isOAuthProvider = provider.metadata.config_keys.some((key) => key.oauth_flow);
|
||||
|
||||
const isConfigured = provider.is_configured;
|
||||
const headerText = showDeleteConfirmation
|
||||
? `Delete configuration for ${provider.metadata.display_name}`
|
||||
@@ -52,7 +70,28 @@ export default function ProviderConfigurationModal({
|
||||
? isActiveProvider
|
||||
? `You cannot delete this provider while it's currently in use. Please switch to a different model first.`
|
||||
: 'This will permanently delete the current provider configuration.'
|
||||
: `Add your API key(s) for this provider to integrate into Goose`;
|
||||
: isOAuthProvider
|
||||
? `Sign in with your ${provider.metadata.display_name} account to use this provider`
|
||||
: `Add your API key(s) for this provider to integrate into goose`;
|
||||
|
||||
const handleOAuthLogin = async () => {
|
||||
setIsOAuthLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await configureProviderOauth({
|
||||
path: { name: provider.name },
|
||||
});
|
||||
if (onConfigured) {
|
||||
onConfigured(provider);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`OAuth login failed: ${formatErrorMessage(err)}`);
|
||||
} finally {
|
||||
setIsOAuthLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitForm = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -91,7 +130,7 @@ export default function ProviderConfigurationModal({
|
||||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
setError(`${error}`);
|
||||
setError(formatErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,38 +218,70 @@ export default function ProviderConfigurationModal({
|
||||
{/* Contains information used to set up each provider */}
|
||||
{/* Only show the form when NOT in delete confirmation mode */}
|
||||
{!showDeleteConfirmation ? (
|
||||
<>
|
||||
{/* Contains information used to set up each provider */}
|
||||
<DefaultProviderSetupForm
|
||||
configValues={configValues}
|
||||
setConfigValues={setConfigValues}
|
||||
provider={provider}
|
||||
validationErrors={validationErrors}
|
||||
/>
|
||||
isOAuthProvider ? (
|
||||
<div className="flex flex-col items-center gap-4 py-6">
|
||||
<Button
|
||||
onClick={handleOAuthLogin}
|
||||
disabled={isOAuthLoading}
|
||||
className="flex items-center gap-2 px-6 py-3"
|
||||
size="lg"
|
||||
>
|
||||
<LogIn size={20} />
|
||||
{isOAuthLoading
|
||||
? 'Signing in...'
|
||||
: `Sign in with ${provider.metadata.display_name}`}
|
||||
</Button>
|
||||
<p className="text-sm text-textSubtle text-center">
|
||||
A browser window will open for you to complete the login.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Contains information used to set up each provider */}
|
||||
<DefaultProviderSetupForm
|
||||
configValues={configValues}
|
||||
setConfigValues={setConfigValues}
|
||||
provider={provider}
|
||||
validationErrors={validationErrors}
|
||||
/>
|
||||
|
||||
{requiredParameters.length > 0 &&
|
||||
provider.metadata.config_keys &&
|
||||
provider.metadata.config_keys.length > 0 && <SecureStorageNotice />}
|
||||
</>
|
||||
{requiredParameters.length > 0 &&
|
||||
provider.metadata.config_keys &&
|
||||
provider.metadata.config_keys.length > 0 && <SecureStorageNotice />}
|
||||
</>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<ProviderSetupActions
|
||||
requiredParameters={requiredParameters}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSubmitForm}
|
||||
onDelete={handleDelete}
|
||||
showDeleteConfirmation={showDeleteConfirmation}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
onCancelDelete={() => {
|
||||
setIsActiveProvider(false);
|
||||
setShowDeleteConfirmation(false);
|
||||
}}
|
||||
canDelete={isConfigured && !isActiveProvider}
|
||||
providerName={provider.metadata.display_name}
|
||||
isActiveProvider={isActiveProvider}
|
||||
/>
|
||||
{isOAuthProvider && !showDeleteConfirmation ? (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
{isConfigured && (
|
||||
<Button variant="destructive" onClick={handleDelete}>
|
||||
Remove Configuration
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ProviderSetupActions
|
||||
requiredParameters={requiredParameters}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSubmitForm}
|
||||
onDelete={handleDelete}
|
||||
showDeleteConfirmation={showDeleteConfirmation}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
onCancelDelete={() => {
|
||||
setIsActiveProvider(false);
|
||||
setShowDeleteConfirmation(false);
|
||||
}}
|
||||
canDelete={isConfigured && !isActiveProvider}
|
||||
providerName={provider.metadata.display_name}
|
||||
isActiveProvider={isActiveProvider}
|
||||
/>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -69,7 +69,7 @@ export type AnalyticsEvent =
|
||||
| { name: 'onboarding_started'; properties: Record<string, never> }
|
||||
| {
|
||||
name: 'onboarding_provider_selected';
|
||||
properties: { method: 'api_key' | 'openrouter' | 'tetrate' | 'ollama' | 'other' };
|
||||
properties: { method: 'api_key' | 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'ollama' | 'other' };
|
||||
}
|
||||
| {
|
||||
name: 'onboarding_completed';
|
||||
@@ -78,7 +78,7 @@ export type AnalyticsEvent =
|
||||
| { name: 'onboarding_abandoned'; properties: { step: string; duration_seconds?: number } }
|
||||
| {
|
||||
name: 'onboarding_setup_failed';
|
||||
properties: { provider: 'openrouter' | 'tetrate'; error_message?: string };
|
||||
properties: { provider: 'openrouter' | 'tetrate' | 'chatgpt_codex'; error_message?: string };
|
||||
}
|
||||
| {
|
||||
name: 'error_occurred';
|
||||
@@ -282,7 +282,7 @@ export function trackOnboardingStarted(): void {
|
||||
}
|
||||
|
||||
export function trackOnboardingProviderSelected(
|
||||
method: 'api_key' | 'openrouter' | 'tetrate' | 'ollama' | 'other'
|
||||
method: 'api_key' | 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'ollama' | 'other'
|
||||
): void {
|
||||
trackEvent({
|
||||
name: 'onboarding_provider_selected',
|
||||
@@ -315,7 +315,7 @@ export function trackOnboardingAbandoned(step: string): void {
|
||||
}
|
||||
|
||||
export function trackOnboardingSetupFailed(
|
||||
provider: 'openrouter' | 'tetrate',
|
||||
provider: 'openrouter' | 'tetrate' | 'chatgpt_codex',
|
||||
errorMessage?: string
|
||||
): void {
|
||||
trackEvent({
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { configureProviderOauth } from '../api';
|
||||
|
||||
export async function startChatGptCodexSetup(): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await configureProviderOauth({
|
||||
path: { name: 'chatgpt_codex' },
|
||||
throwOnError: true,
|
||||
});
|
||||
return { success: true, message: 'ChatGPT Codex setup completed' };
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to start ChatGPT Codex setup: ${e}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user