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:
Michael Neale
2026-01-23 17:11:58 +11:00
committed by GitHub
parent e7bfdf8fa2
commit e78a1e7d4e
26 changed files with 1666 additions and 69 deletions
Generated
+7
View File
@@ -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"
+2 -11
View File
@@ -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
+1
View File
@@ -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)
}
+10
View File
@@ -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
+1
View File
@@ -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
+6 -2
View File
@@ -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
View File
@@ -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);
+1
View File
@@ -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;
+3 -1
View File
@@ -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"'") |
+27
View File
@@ -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
+26
View File
@@ -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;
+7 -8
View File
@@ -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
+99 -6
View File
@@ -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>
+4 -4
View File
@@ -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({
+16
View File
@@ -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}`,
};
}
}