feat: use AI Providers for Claude Code authentication
Replace the separate API key field in Claude Code backend settings with integration into the AI Providers system: - Add use_for_backends field to provider data model (stored in opencode.json) - Add backend selection step in Add Provider modal for Anthropic (OpenCode/Claude Code) - Add /api/ai/providers/for-backend/:id endpoint to get provider credentials - Update mission runner to get Anthropic API key from provider system - Replace API key input with provider status display in Claude Code settings - Show backend badges on provider cards in AI Providers section This allows users to authenticate Claude Code using: - Claude Pro/Max subscription via OAuth - Anthropic API key through the unified provider system
This commit is contained in:
@@ -19,6 +19,8 @@ import {
|
|||||||
listBackends,
|
listBackends,
|
||||||
getBackendConfig,
|
getBackendConfig,
|
||||||
updateBackendConfig,
|
updateBackendConfig,
|
||||||
|
getProviderForBackend,
|
||||||
|
BackendProviderResponse,
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import {
|
import {
|
||||||
Server,
|
Server,
|
||||||
@@ -169,6 +171,13 @@ export default function SettingsPage() {
|
|||||||
{ revalidateOnFocus: false }
|
{ revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Fetch Claude Code provider status (Anthropic provider configured for claudecode)
|
||||||
|
const { data: claudecodeProvider, mutate: mutateClaudeProvider } = useSWR<BackendProviderResponse>(
|
||||||
|
'claudecode-provider',
|
||||||
|
() => getProviderForBackend('claudecode'),
|
||||||
|
{ revalidateOnFocus: false }
|
||||||
|
);
|
||||||
|
|
||||||
// Check if there are unsaved changes
|
// Check if there are unsaved changes
|
||||||
const hasUnsavedChanges = apiUrl !== originalValues.apiUrl;
|
const hasUnsavedChanges = apiUrl !== originalValues.apiUrl;
|
||||||
|
|
||||||
@@ -354,15 +363,11 @@ export default function SettingsPage() {
|
|||||||
default_model: claudeForm.default_model || null,
|
default_model: claudeForm.default_model || null,
|
||||||
cli_path: claudeForm.cli_path || null,
|
cli_path: claudeForm.cli_path || null,
|
||||||
};
|
};
|
||||||
if (claudeForm.api_key) {
|
|
||||||
settings.api_key = claudeForm.api_key;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await updateBackendConfig('claudecode', settings, {
|
const result = await updateBackendConfig('claudecode', settings, {
|
||||||
enabled: claudeForm.enabled,
|
enabled: claudeForm.enabled,
|
||||||
});
|
});
|
||||||
toast.success(result.message || 'Claude Code settings updated');
|
toast.success(result.message || 'Claude Code settings updated');
|
||||||
setClaudeForm((prev) => ({ ...prev, api_key: '' }));
|
|
||||||
mutateClaudeBackend();
|
mutateClaudeBackend();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(
|
toast.error(
|
||||||
@@ -511,7 +516,7 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* AI Providers */}
|
{/* AI Providers */}
|
||||||
<div className="rounded-xl bg-white/[0.02] border border-white/[0.04] p-5">
|
<div id="ai-providers" className="rounded-xl bg-white/[0.02] border border-white/[0.04] p-5">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-violet-500/10">
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-violet-500/10">
|
||||||
@@ -520,7 +525,7 @@ export default function SettingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-sm font-medium text-white">AI Providers</h2>
|
<h2 className="text-sm font-medium text-white">AI Providers</h2>
|
||||||
<p className="text-xs text-white/40">
|
<p className="text-xs text-white/40">
|
||||||
Configure inference providers for OpenCode
|
Configure inference providers for OpenCode and Claude Code
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -624,6 +629,20 @@ export default function SettingsPage() {
|
|||||||
<span className="text-base">{config.icon}</span>
|
<span className="text-base">{config.icon}</span>
|
||||||
<span className="text-sm text-white/80 flex-1 truncate">{provider.name}</span>
|
<span className="text-sm text-white/80 flex-1 truncate">{provider.name}</span>
|
||||||
|
|
||||||
|
{/* Backend badges */}
|
||||||
|
{provider.use_for_backends && provider.use_for_backends.length > 0 && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{provider.use_for_backends.map((backend) => (
|
||||||
|
<span
|
||||||
|
key={backend}
|
||||||
|
className="px-1.5 py-0.5 text-[10px] rounded bg-white/[0.06] text-white/50"
|
||||||
|
>
|
||||||
|
{backend === 'claudecode' ? 'Claude' : backend === 'opencode' ? 'OC' : backend}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Status indicators */}
|
{/* Status indicators */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{provider.is_default && (
|
{provider.is_default && (
|
||||||
@@ -799,26 +818,57 @@ export default function SettingsPage() {
|
|||||||
Enabled
|
Enabled
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-white/50">
|
{/* Anthropic Provider Status */}
|
||||||
API key status:{' '}
|
<div className="rounded-lg border border-white/[0.06] bg-white/[0.02] p-3">
|
||||||
<span className={claudeForm.api_key_configured ? 'text-emerald-400' : 'text-amber-400'}>
|
<div className="flex items-center justify-between">
|
||||||
{claudeForm.api_key_configured ? 'Configured' : 'Not configured'}
|
<div className="flex items-center gap-2">
|
||||||
</span>
|
<span className="text-xl">🧠</span>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<div className="text-sm text-white">
|
||||||
<label className="block text-xs text-white/60 mb-1.5">API Key</label>
|
{claudecodeProvider?.configured
|
||||||
<input
|
? claudecodeProvider.provider_name || 'Anthropic'
|
||||||
type="password"
|
: 'Anthropic Provider'}
|
||||||
value={claudeForm.api_key}
|
</div>
|
||||||
onChange={(e) =>
|
<div className="text-xs text-white/40">
|
||||||
setClaudeForm((prev) => ({ ...prev, api_key: e.target.value }))
|
{claudecodeProvider?.configured
|
||||||
}
|
? claudecodeProvider.oauth
|
||||||
placeholder="sk-..."
|
? 'Connected via OAuth'
|
||||||
className="w-full rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-sm text-white focus:outline-none focus:border-indigo-500/50"
|
: claudecodeProvider.api_key
|
||||||
/>
|
? 'Using API key'
|
||||||
<p className="mt-1.5 text-xs text-white/30">
|
: 'Configured'
|
||||||
Stored securely in the secrets vault; leave blank to keep existing key.
|
: 'Not configured for Claude Code'}
|
||||||
</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{claudecodeProvider?.configured && claudecodeProvider.has_credentials ? (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-emerald-400">
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
Connected
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-amber-400">
|
||||||
|
<AlertTriangle className="h-3.5 w-3.5" />
|
||||||
|
Not connected
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!claudecodeProvider?.configured && (
|
||||||
|
<p className="mt-2 text-xs text-white/50">
|
||||||
|
Add an Anthropic provider in{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
document.getElementById('ai-providers')?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}}
|
||||||
|
className="text-indigo-400 hover:text-indigo-300 underline"
|
||||||
|
>
|
||||||
|
AI Providers
|
||||||
|
</button>{' '}
|
||||||
|
and select "Claude Code" as a target backend.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-white/60 mb-1.5">Default Model</label>
|
<label className="block text-xs text-white/60 mb-1.5">Default Model</label>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const getProviderAuthMethods = (providerType: AIProviderType): AIProviderAuthMet
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModalStep = 'select-provider' | 'select-method' | 'enter-api-key' | 'oauth-callback';
|
type ModalStep = 'select-provider' | 'select-method' | 'select-backends' | 'enter-api-key' | 'oauth-callback';
|
||||||
|
|
||||||
export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: AddProviderModalProps) {
|
export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: AddProviderModalProps) {
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -90,6 +90,8 @@ export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: Ad
|
|||||||
const [oauthResponse, setOauthResponse] = useState<OAuthAuthorizeResponse | null>(null);
|
const [oauthResponse, setOauthResponse] = useState<OAuthAuthorizeResponse | null>(null);
|
||||||
const [oauthCode, setOauthCode] = useState('');
|
const [oauthCode, setOauthCode] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
// Backend selection for Anthropic (OpenCode and/or Claude Code)
|
||||||
|
const [selectedBackends, setSelectedBackends] = useState<string[]>(['opencode']);
|
||||||
|
|
||||||
// Get selected provider info
|
// Get selected provider info
|
||||||
const selectedTypeInfo = selectedProvider ? providerTypes.find(t => t.id === selectedProvider) : null;
|
const selectedTypeInfo = selectedProvider ? providerTypes.find(t => t.id === selectedProvider) : null;
|
||||||
@@ -106,6 +108,7 @@ export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: Ad
|
|||||||
setOauthResponse(null);
|
setOauthResponse(null);
|
||||||
setOauthCode('');
|
setOauthCode('');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setSelectedBackends(['opencode']);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
@@ -155,6 +158,12 @@ export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: Ad
|
|||||||
const method = authMethods[methodIndex];
|
const method = authMethods[methodIndex];
|
||||||
setSelectedMethodIndex(methodIndex);
|
setSelectedMethodIndex(methodIndex);
|
||||||
|
|
||||||
|
// For Anthropic, show backend selection step first
|
||||||
|
if (selectedProvider === 'anthropic') {
|
||||||
|
setStep('select-backends');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (method.type === 'api') {
|
if (method.type === 'api') {
|
||||||
setStep('enter-api-key');
|
setStep('enter-api-key');
|
||||||
} else {
|
} else {
|
||||||
@@ -173,6 +182,39 @@ export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: Ad
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContinueFromBackends = async () => {
|
||||||
|
if (selectedBackends.length === 0) {
|
||||||
|
toast.error('Please select at least one backend');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = authMethods[selectedMethodIndex!];
|
||||||
|
if (method.type === 'api') {
|
||||||
|
setStep('enter-api-key');
|
||||||
|
} else {
|
||||||
|
// Start OAuth flow
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await oauthAuthorize(selectedProvider!, selectedMethodIndex!);
|
||||||
|
setOauthResponse(response);
|
||||||
|
setStep('oauth-callback');
|
||||||
|
window.open(response.url, '_blank');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`Failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleBackend = (backendId: string) => {
|
||||||
|
setSelectedBackends(prev =>
|
||||||
|
prev.includes(backendId)
|
||||||
|
? prev.filter(b => b !== backendId)
|
||||||
|
: [...prev, backendId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmitApiKey = async () => {
|
const handleSubmitApiKey = async () => {
|
||||||
if (!apiKey.trim() || !selectedProvider) return;
|
if (!apiKey.trim() || !selectedProvider) return;
|
||||||
|
|
||||||
@@ -182,6 +224,8 @@ export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: Ad
|
|||||||
provider_type: selectedProvider,
|
provider_type: selectedProvider,
|
||||||
name: selectedTypeInfo?.name || selectedProvider,
|
name: selectedTypeInfo?.name || selectedProvider,
|
||||||
api_key: apiKey,
|
api_key: apiKey,
|
||||||
|
// Include backend targeting for Anthropic
|
||||||
|
use_for_backends: selectedProvider === 'anthropic' ? selectedBackends : undefined,
|
||||||
});
|
});
|
||||||
toast.success('Provider added');
|
toast.success('Provider added');
|
||||||
onSuccess();
|
onSuccess();
|
||||||
@@ -198,7 +242,13 @@ export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: Ad
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await oauthCallback(selectedProvider, selectedMethodIndex, oauthCode);
|
await oauthCallback(
|
||||||
|
selectedProvider,
|
||||||
|
selectedMethodIndex,
|
||||||
|
oauthCode,
|
||||||
|
// Include backend targeting for Anthropic
|
||||||
|
selectedProvider === 'anthropic' ? selectedBackends : undefined
|
||||||
|
);
|
||||||
toast.success('Provider connected');
|
toast.success('Provider connected');
|
||||||
onSuccess();
|
onSuccess();
|
||||||
onClose();
|
onClose();
|
||||||
@@ -213,8 +263,12 @@ export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: Ad
|
|||||||
if (step === 'select-method') {
|
if (step === 'select-method') {
|
||||||
setStep('select-provider');
|
setStep('select-provider');
|
||||||
setSelectedProvider(null);
|
setSelectedProvider(null);
|
||||||
|
} else if (step === 'select-backends') {
|
||||||
|
setStep('select-method');
|
||||||
} else if (step === 'enter-api-key') {
|
} else if (step === 'enter-api-key') {
|
||||||
if (hasOAuth) {
|
if (selectedProvider === 'anthropic') {
|
||||||
|
setStep('select-backends');
|
||||||
|
} else if (hasOAuth) {
|
||||||
setStep('select-method');
|
setStep('select-method');
|
||||||
} else {
|
} else {
|
||||||
setStep('select-provider');
|
setStep('select-provider');
|
||||||
@@ -230,6 +284,7 @@ export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: Ad
|
|||||||
switch (step) {
|
switch (step) {
|
||||||
case 'select-provider': return 'Add Provider';
|
case 'select-provider': return 'Add Provider';
|
||||||
case 'select-method': return `Connect ${selectedTypeInfo?.name}`;
|
case 'select-method': return `Connect ${selectedTypeInfo?.name}`;
|
||||||
|
case 'select-backends': return 'Select Backends';
|
||||||
case 'enter-api-key': return `${selectedTypeInfo?.name} API Key`;
|
case 'enter-api-key': return `${selectedTypeInfo?.name} API Key`;
|
||||||
case 'oauth-callback': return 'Complete Authorization';
|
case 'oauth-callback': return 'Complete Authorization';
|
||||||
default: return 'Add Provider';
|
default: return 'Add Provider';
|
||||||
@@ -323,6 +378,48 @@ export function AddProviderModal({ open, onClose, onSuccess, providerTypes }: Ad
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Step 2.5: Select Backends (Anthropic only) */}
|
||||||
|
{step === 'select-backends' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-white/60">
|
||||||
|
Choose which backends should use this Anthropic provider:
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="flex items-center gap-3 p-3 rounded-xl border border-white/[0.06] hover:bg-white/[0.02] transition-colors cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedBackends.includes('opencode')}
|
||||||
|
onChange={() => toggleBackend('opencode')}
|
||||||
|
className="rounded border-white/20 bg-white/[0.02] text-indigo-500 focus:ring-indigo-500/30 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm text-white">OpenCode</div>
|
||||||
|
<div className="text-xs text-white/40">Use for OpenCode agents and missions</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-3 p-3 rounded-xl border border-white/[0.06] hover:bg-white/[0.02] transition-colors cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedBackends.includes('claudecode')}
|
||||||
|
onChange={() => toggleBackend('claudecode')}
|
||||||
|
className="rounded border-white/20 bg-white/[0.02] text-indigo-500 focus:ring-indigo-500/30 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm text-white">Claude Code</div>
|
||||||
|
<div className="text-xs text-white/40">Use for Claude CLI-based missions</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleContinueFromBackends}
|
||||||
|
disabled={loading || selectedBackends.length === 0}
|
||||||
|
className="w-full rounded-xl bg-indigo-500 px-4 py-3 text-sm font-medium text-white hover:bg-indigo-600 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? <Loader className="h-4 w-4 animate-spin mx-auto" /> : 'Continue'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Step 3: Enter API Key */}
|
{/* Step 3: Enter API Key */}
|
||||||
{step === 'enter-api-key' && (
|
{step === 'enter-api-key' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@@ -2345,6 +2345,8 @@ export interface AIProvider {
|
|||||||
uses_oauth: boolean;
|
uses_oauth: boolean;
|
||||||
auth_methods: AIProviderAuthMethod[];
|
auth_methods: AIProviderAuthMethod[];
|
||||||
status: AIProviderStatus;
|
status: AIProviderStatus;
|
||||||
|
/** Which backends this provider is used for (e.g., ["opencode", "claudecode"]) */
|
||||||
|
use_for_backends: string[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -2389,6 +2391,8 @@ export async function createAIProvider(data: {
|
|||||||
api_key?: string;
|
api_key?: string;
|
||||||
base_url?: string;
|
base_url?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
/** Which backends this provider is used for (e.g., ["opencode", "claudecode"]) */
|
||||||
|
use_for_backends?: string[];
|
||||||
}): Promise<AIProvider> {
|
}): Promise<AIProvider> {
|
||||||
const res = await apiFetch("/api/ai/providers", {
|
const res = await apiFetch("/api/ai/providers", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -2407,6 +2411,8 @@ export async function updateAIProvider(
|
|||||||
api_key?: string | null;
|
api_key?: string | null;
|
||||||
base_url?: string | null;
|
base_url?: string | null;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
/** Which backends this provider is used for (e.g., ["opencode", "claudecode"]) */
|
||||||
|
use_for_backends?: string[];
|
||||||
}
|
}
|
||||||
): Promise<AIProvider> {
|
): Promise<AIProvider> {
|
||||||
const res = await apiFetch(`/api/ai/providers/${id}`, {
|
const res = await apiFetch(`/api/ai/providers/${id}`, {
|
||||||
@@ -2424,6 +2430,27 @@ export async function deleteAIProvider(id: string): Promise<void> {
|
|||||||
if (!res.ok) throw new Error("Failed to delete AI provider");
|
if (!res.ok) throw new Error("Failed to delete AI provider");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provider credentials for a backend
|
||||||
|
export interface BackendProviderResponse {
|
||||||
|
configured: boolean;
|
||||||
|
provider_type: string | null;
|
||||||
|
provider_name: string | null;
|
||||||
|
api_key: string | null;
|
||||||
|
oauth: {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
expires_at: number;
|
||||||
|
} | null;
|
||||||
|
has_credentials: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provider credentials for a specific backend (e.g., "claudecode")
|
||||||
|
export async function getProviderForBackend(backendId: string): Promise<BackendProviderResponse> {
|
||||||
|
const res = await apiFetch(`/api/ai/providers/for-backend/${backendId}`);
|
||||||
|
if (!res.ok) throw new Error("Failed to get provider for backend");
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate provider (initiate OAuth or check API key)
|
// Authenticate provider (initiate OAuth or check API key)
|
||||||
export async function authenticateAIProvider(id: string): Promise<AIProviderAuthResponse> {
|
export async function authenticateAIProvider(id: string): Promise<AIProviderAuthResponse> {
|
||||||
const res = await apiFetch(`/api/ai/providers/${id}/auth`, { method: "POST" });
|
const res = await apiFetch(`/api/ai/providers/${id}/auth`, { method: "POST" });
|
||||||
@@ -2460,11 +2487,20 @@ export async function oauthAuthorize(id: string, methodIndex: number): Promise<O
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Complete OAuth flow with authorization code
|
// Complete OAuth flow with authorization code
|
||||||
export async function oauthCallback(id: string, methodIndex: number, code: string): Promise<AIProvider> {
|
export async function oauthCallback(
|
||||||
|
id: string,
|
||||||
|
methodIndex: number,
|
||||||
|
code: string,
|
||||||
|
useForBackends?: string[]
|
||||||
|
): Promise<AIProvider> {
|
||||||
const res = await apiFetch(`/api/ai/providers/${id}/oauth/callback`, {
|
const res = await apiFetch(`/api/ai/providers/${id}/oauth/callback`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ method_index: methodIndex, code }),
|
body: JSON.stringify({
|
||||||
|
method_index: methodIndex,
|
||||||
|
code,
|
||||||
|
use_for_backends: useForBackends,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await res.text();
|
const error = await res.text();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
//! - Delete provider
|
//! - Delete provider
|
||||||
//! - Authenticate provider (OAuth flow)
|
//! - Authenticate provider (OAuth flow)
|
||||||
//! - Set default provider
|
//! - Set default provider
|
||||||
|
//! - Get provider credentials for specific backend (Claude Code)
|
||||||
|
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeSet, HashMap};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -116,6 +117,7 @@ pub fn routes() -> Router<Arc<super::routes::AppState>> {
|
|||||||
.route("/types", get(list_provider_types))
|
.route("/types", get(list_provider_types))
|
||||||
.route("/opencode-auth", get(get_opencode_auth))
|
.route("/opencode-auth", get(get_opencode_auth))
|
||||||
.route("/opencode-auth", post(set_opencode_auth))
|
.route("/opencode-auth", post(set_opencode_auth))
|
||||||
|
.route("/for-backend/:backend_id", get(get_provider_for_backend))
|
||||||
.route("/:id", get(get_provider))
|
.route("/:id", get(get_provider))
|
||||||
.route("/:id", put(update_provider))
|
.route("/:id", put(update_provider))
|
||||||
.route("/:id", delete(delete_provider))
|
.route("/:id", delete(delete_provider))
|
||||||
@@ -126,6 +128,75 @@ pub fn routes() -> Router<Arc<super::routes::AppState>> {
|
|||||||
.route("/:id/default", post(set_default))
|
.route("/:id/default", post(set_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Public API for Backend Access
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Get the Anthropic API key for the Claude Code backend.
|
||||||
|
///
|
||||||
|
/// This checks if the Anthropic provider has "claudecode" in its use_for_backends
|
||||||
|
/// configuration and returns the API key if available.
|
||||||
|
///
|
||||||
|
/// Returns None if:
|
||||||
|
/// - Anthropic provider is not configured for claudecode
|
||||||
|
/// - No API key is set (OAuth-only auth)
|
||||||
|
/// - Any error occurs reading the config
|
||||||
|
pub fn get_anthropic_api_key_for_claudecode(working_dir: &Path) -> Option<String> {
|
||||||
|
// Read the OpenCode config to check use_for_backends
|
||||||
|
let config_path = get_opencode_config_path(working_dir);
|
||||||
|
let opencode_config = read_opencode_config(&config_path).ok()?;
|
||||||
|
|
||||||
|
// Check if Anthropic provider has claudecode in use_for_backends
|
||||||
|
let anthropic_config = get_provider_config_entry(&opencode_config, ProviderType::Anthropic);
|
||||||
|
let use_for_claudecode = anthropic_config
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.use_for_backends.as_ref())
|
||||||
|
.map(|backends| backends.iter().any(|b| b == "claudecode"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !use_for_claudecode {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the API key from auth.json
|
||||||
|
let auth = read_opencode_auth().ok()?;
|
||||||
|
let anthropic_auth = auth.get("anthropic")?;
|
||||||
|
|
||||||
|
// Check for API key (not OAuth)
|
||||||
|
let auth_type = anthropic_auth.get("type").and_then(|v| v.as_str());
|
||||||
|
match auth_type {
|
||||||
|
Some("api_key") | Some("api") => {
|
||||||
|
anthropic_auth
|
||||||
|
.get("key")
|
||||||
|
.or_else(|| anthropic_auth.get("api_key"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Also check without type field
|
||||||
|
anthropic_auth
|
||||||
|
.get("key")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the Anthropic provider is configured for the Claude Code backend.
|
||||||
|
pub fn is_anthropic_configured_for_claudecode(working_dir: &Path) -> bool {
|
||||||
|
let config_path = get_opencode_config_path(working_dir);
|
||||||
|
let Ok(opencode_config) = read_opencode_config(&config_path) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let anthropic_config = get_provider_config_entry(&opencode_config, ProviderType::Anthropic);
|
||||||
|
anthropic_config
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.use_for_backends.as_ref())
|
||||||
|
.map(|backends| backends.iter().any(|b| b == "claudecode"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Request/Response Types
|
// Request/Response Types
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -148,6 +219,10 @@ pub struct CreateProviderRequest {
|
|||||||
pub base_url: Option<String>,
|
pub base_url: Option<String>,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
/// Which backends this provider is used for (e.g., ["opencode", "claudecode"])
|
||||||
|
/// Only applicable for Anthropic provider. Defaults to ["opencode"].
|
||||||
|
#[serde(default)]
|
||||||
|
pub use_for_backends: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
@@ -160,6 +235,8 @@ pub struct UpdateProviderRequest {
|
|||||||
pub api_key: Option<Option<String>>,
|
pub api_key: Option<Option<String>>,
|
||||||
pub base_url: Option<Option<String>>,
|
pub base_url: Option<Option<String>>,
|
||||||
pub enabled: Option<bool>,
|
pub enabled: Option<bool>,
|
||||||
|
/// Which backends this provider is used for (e.g., ["opencode", "claudecode"])
|
||||||
|
pub use_for_backends: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -176,6 +253,8 @@ pub struct ProviderResponse {
|
|||||||
pub uses_oauth: bool,
|
pub uses_oauth: bool,
|
||||||
pub auth_methods: Vec<AuthMethod>,
|
pub auth_methods: Vec<AuthMethod>,
|
||||||
pub status: ProviderStatusResponse,
|
pub status: ProviderStatusResponse,
|
||||||
|
/// Which backends this provider is used for (e.g., ["opencode", "claudecode"])
|
||||||
|
pub use_for_backends: Vec<String>,
|
||||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
@@ -200,6 +279,8 @@ struct ProviderConfigEntry {
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
base_url: Option<String>,
|
base_url: Option<String>,
|
||||||
enabled: Option<bool>,
|
enabled: Option<bool>,
|
||||||
|
/// Which backends this provider is used for (e.g., ["opencode", "claudecode"])
|
||||||
|
use_for_backends: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_provider_response(
|
fn build_provider_response(
|
||||||
@@ -229,6 +310,17 @@ fn build_provider_response(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For Anthropic, use configured backends or default to ["opencode"]
|
||||||
|
// For other providers, always use ["opencode"]
|
||||||
|
let use_for_backends = if provider_type == ProviderType::Anthropic {
|
||||||
|
config
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.use_for_backends.clone())
|
||||||
|
.unwrap_or_else(|| vec!["opencode".to_string()])
|
||||||
|
} else {
|
||||||
|
vec!["opencode".to_string()]
|
||||||
|
};
|
||||||
|
|
||||||
ProviderResponse {
|
ProviderResponse {
|
||||||
id: provider_type.id().to_string(),
|
id: provider_type.id().to_string(),
|
||||||
provider_type,
|
provider_type,
|
||||||
@@ -242,6 +334,7 @@ fn build_provider_response(
|
|||||||
uses_oauth: provider_type.uses_oauth(),
|
uses_oauth: provider_type.uses_oauth(),
|
||||||
auth_methods: provider_type.auth_methods(),
|
auth_methods: provider_type.auth_methods(),
|
||||||
status,
|
status,
|
||||||
|
use_for_backends,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
}
|
}
|
||||||
@@ -255,6 +348,31 @@ pub struct AuthResponse {
|
|||||||
pub auth_url: Option<String>,
|
pub auth_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Response for provider credentials for a specific backend.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct BackendProviderResponse {
|
||||||
|
/// Whether a provider is configured for this backend
|
||||||
|
pub configured: bool,
|
||||||
|
/// The provider type (e.g., "anthropic")
|
||||||
|
pub provider_type: Option<String>,
|
||||||
|
/// The provider name
|
||||||
|
pub provider_name: Option<String>,
|
||||||
|
/// API key (if using API key auth)
|
||||||
|
pub api_key: Option<String>,
|
||||||
|
/// OAuth credentials (if using OAuth)
|
||||||
|
pub oauth: Option<BackendOAuthCredentials>,
|
||||||
|
/// Whether the provider has valid credentials
|
||||||
|
pub has_credentials: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OAuth credentials for backend provider.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct BackendOAuthCredentials {
|
||||||
|
pub access_token: String,
|
||||||
|
pub refresh_token: String,
|
||||||
|
pub expires_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
/// Request to initiate OAuth authorization.
|
/// Request to initiate OAuth authorization.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct OAuthAuthorizeRequest {
|
pub struct OAuthAuthorizeRequest {
|
||||||
@@ -280,6 +398,8 @@ pub struct OAuthCallbackRequest {
|
|||||||
pub method_index: usize,
|
pub method_index: usize,
|
||||||
/// Authorization code from the OAuth flow
|
/// Authorization code from the OAuth flow
|
||||||
pub code: String,
|
pub code: String,
|
||||||
|
/// Which backends to use this provider for (e.g., ["opencode", "claudecode"])
|
||||||
|
pub use_for_backends: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request to set OpenCode auth credentials directly.
|
/// Request to set OpenCode auth credentials directly.
|
||||||
@@ -654,10 +774,19 @@ fn get_provider_config_entry(
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(|s| s.to_string());
|
.map(|s| s.to_string());
|
||||||
let enabled = entry.get("enabled").and_then(|v| v.as_bool());
|
let enabled = entry.get("enabled").and_then(|v| v.as_bool());
|
||||||
|
let use_for_backends = entry
|
||||||
|
.get("useForBackends")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
Some(ProviderConfigEntry {
|
Some(ProviderConfigEntry {
|
||||||
name,
|
name,
|
||||||
base_url,
|
base_url,
|
||||||
enabled,
|
enabled,
|
||||||
|
use_for_backends,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,6 +796,7 @@ fn set_provider_config_entry(
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
base_url: Option<Option<String>>,
|
base_url: Option<Option<String>>,
|
||||||
enabled: Option<bool>,
|
enabled: Option<bool>,
|
||||||
|
use_for_backends: Option<Vec<String>>,
|
||||||
) {
|
) {
|
||||||
if !config.is_object() {
|
if !config.is_object() {
|
||||||
*config = serde_json::json!({});
|
*config = serde_json::json!({});
|
||||||
@@ -707,6 +837,18 @@ fn set_provider_config_entry(
|
|||||||
// We treat providers as enabled when present and avoid writing this field.
|
// We treat providers as enabled when present and avoid writing this field.
|
||||||
let _ = enabled;
|
let _ = enabled;
|
||||||
entry_obj.remove("enabled");
|
entry_obj.remove("enabled");
|
||||||
|
|
||||||
|
// Store which backends this provider is used for (only for Anthropic)
|
||||||
|
if let Some(backends) = use_for_backends {
|
||||||
|
let backends_json: Vec<serde_json::Value> = backends
|
||||||
|
.into_iter()
|
||||||
|
.map(serde_json::Value::String)
|
||||||
|
.collect();
|
||||||
|
entry_obj.insert(
|
||||||
|
"useForBackends".to_string(),
|
||||||
|
serde_json::Value::Array(backends_json),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_provider_config_entry(config: &mut serde_json::Value, provider: ProviderType) {
|
fn remove_provider_config_entry(config: &mut serde_json::Value, provider: ProviderType) {
|
||||||
@@ -1024,6 +1166,128 @@ async fn list_providers(
|
|||||||
Ok(Json(providers))
|
Ok(Json(providers))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GET /api/ai/providers/for-backend/:backend_id - Get provider credentials for a specific backend.
|
||||||
|
///
|
||||||
|
/// For Claude Code backend, this returns the Anthropic provider that has "claudecode" in use_for_backends.
|
||||||
|
async fn get_provider_for_backend(
|
||||||
|
State(state): State<Arc<super::routes::AppState>>,
|
||||||
|
AxumPath(backend_id): AxumPath<String>,
|
||||||
|
) -> Result<Json<BackendProviderResponse>, (StatusCode, String)> {
|
||||||
|
// Currently only "claudecode" backend uses this endpoint
|
||||||
|
if backend_id != "claudecode" {
|
||||||
|
return Ok(Json(BackendProviderResponse {
|
||||||
|
configured: false,
|
||||||
|
provider_type: None,
|
||||||
|
provider_name: None,
|
||||||
|
api_key: None,
|
||||||
|
oauth: None,
|
||||||
|
has_credentials: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the OpenCode config to find provider with claudecode in use_for_backends
|
||||||
|
let config_path = get_opencode_config_path(&state.config.working_dir);
|
||||||
|
let opencode_config =
|
||||||
|
read_opencode_config(&config_path).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||||
|
|
||||||
|
// Check if Anthropic provider has claudecode in use_for_backends
|
||||||
|
let anthropic_config = get_provider_config_entry(&opencode_config, ProviderType::Anthropic);
|
||||||
|
let use_for_claudecode = anthropic_config
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.use_for_backends.as_ref())
|
||||||
|
.map(|backends| backends.iter().any(|b| b == "claudecode"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !use_for_claudecode {
|
||||||
|
return Ok(Json(BackendProviderResponse {
|
||||||
|
configured: false,
|
||||||
|
provider_type: None,
|
||||||
|
provider_name: None,
|
||||||
|
api_key: None,
|
||||||
|
oauth: None,
|
||||||
|
has_credentials: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the Anthropic provider credentials from auth.json
|
||||||
|
let auth = read_opencode_auth().map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||||
|
let anthropic_auth = auth.get("anthropic");
|
||||||
|
|
||||||
|
let (api_key, oauth, has_credentials) = if let Some(auth_entry) = anthropic_auth {
|
||||||
|
let auth_type = auth_entry.get("type").and_then(|v| v.as_str());
|
||||||
|
match auth_type {
|
||||||
|
Some("api_key") | Some("api") => {
|
||||||
|
let key = auth_entry
|
||||||
|
.get("key")
|
||||||
|
.or_else(|| auth_entry.get("api_key"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
(key, None, true)
|
||||||
|
}
|
||||||
|
Some("oauth") => {
|
||||||
|
let oauth_creds = BackendOAuthCredentials {
|
||||||
|
access_token: auth_entry
|
||||||
|
.get("access")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
refresh_token: auth_entry
|
||||||
|
.get("refresh")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
expires_at: auth_entry
|
||||||
|
.get("expires")
|
||||||
|
.and_then(|v| v.as_i64())
|
||||||
|
.unwrap_or(0),
|
||||||
|
};
|
||||||
|
(None, Some(oauth_creds), true)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Check for OAuth credentials without type field
|
||||||
|
if auth_entry.get("refresh").is_some() {
|
||||||
|
let oauth_creds = BackendOAuthCredentials {
|
||||||
|
access_token: auth_entry
|
||||||
|
.get("access")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
refresh_token: auth_entry
|
||||||
|
.get("refresh")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
expires_at: auth_entry
|
||||||
|
.get("expires")
|
||||||
|
.and_then(|v| v.as_i64())
|
||||||
|
.unwrap_or(0),
|
||||||
|
};
|
||||||
|
(None, Some(oauth_creds), true)
|
||||||
|
} else if auth_entry.get("key").is_some() {
|
||||||
|
let key = auth_entry
|
||||||
|
.get("key")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
(key, None, true)
|
||||||
|
} else {
|
||||||
|
(None, None, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, None, false)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(BackendProviderResponse {
|
||||||
|
configured: true,
|
||||||
|
provider_type: Some("anthropic".to_string()),
|
||||||
|
provider_name: anthropic_config.and_then(|c| c.name).or_else(|| Some("Anthropic".to_string())),
|
||||||
|
api_key,
|
||||||
|
oauth,
|
||||||
|
has_credentials,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/// POST /api/ai/providers - Create a new provider.
|
/// POST /api/ai/providers - Create a new provider.
|
||||||
async fn create_provider(
|
async fn create_provider(
|
||||||
State(state): State<Arc<super::routes::AppState>>,
|
State(state): State<Arc<super::routes::AppState>>,
|
||||||
@@ -1045,12 +1309,21 @@ async fn create_provider(
|
|||||||
let mut opencode_config =
|
let mut opencode_config =
|
||||||
read_opencode_config(&config_path).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
read_opencode_config(&config_path).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||||
|
|
||||||
|
// For Anthropic, default use_for_backends to ["opencode"] if not specified
|
||||||
|
let use_for_backends = if provider_type == ProviderType::Anthropic {
|
||||||
|
req.use_for_backends
|
||||||
|
.or_else(|| Some(vec!["opencode".to_string()]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
set_provider_config_entry(
|
set_provider_config_entry(
|
||||||
&mut opencode_config,
|
&mut opencode_config,
|
||||||
provider_type,
|
provider_type,
|
||||||
Some(req.name),
|
Some(req.name),
|
||||||
Some(req.base_url),
|
Some(req.base_url),
|
||||||
Some(req.enabled),
|
Some(req.enabled),
|
||||||
|
use_for_backends,
|
||||||
);
|
);
|
||||||
|
|
||||||
write_opencode_config(&config_path, &opencode_config)
|
write_opencode_config(&config_path, &opencode_config)
|
||||||
@@ -1135,6 +1408,7 @@ async fn update_provider(
|
|||||||
req.name,
|
req.name,
|
||||||
req.base_url,
|
req.base_url,
|
||||||
req.enabled,
|
req.enabled,
|
||||||
|
req.use_for_backends,
|
||||||
);
|
);
|
||||||
|
|
||||||
write_opencode_config(&config_path, &opencode_config)
|
write_opencode_config(&config_path, &opencode_config)
|
||||||
@@ -1635,8 +1909,24 @@ async fn oauth_callback_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let config_path = get_opencode_config_path(&state.config.working_dir);
|
let config_path = get_opencode_config_path(&state.config.working_dir);
|
||||||
let opencode_config = read_opencode_config(&config_path)
|
let mut opencode_config = read_opencode_config(&config_path)
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||||
|
|
||||||
|
// Update use_for_backends if specified
|
||||||
|
if req.use_for_backends.is_some() {
|
||||||
|
set_provider_config_entry(
|
||||||
|
&mut opencode_config,
|
||||||
|
provider_type,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
req.use_for_backends.clone(),
|
||||||
|
);
|
||||||
|
if let Err(e) = write_opencode_config(&config_path, &opencode_config) {
|
||||||
|
tracing::error!("Failed to write OpenCode config: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let default_provider = get_default_provider(&opencode_config);
|
let default_provider = get_default_provider(&opencode_config);
|
||||||
let config_entry = get_provider_config_entry(&opencode_config, provider_type);
|
let config_entry = get_provider_config_entry(&opencode_config, provider_type);
|
||||||
let response = build_provider_response(
|
let response = build_provider_response(
|
||||||
@@ -1683,8 +1973,24 @@ async fn oauth_callback_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let config_path = get_opencode_config_path(&state.config.working_dir);
|
let config_path = get_opencode_config_path(&state.config.working_dir);
|
||||||
let opencode_config = read_opencode_config(&config_path)
|
let mut opencode_config = read_opencode_config(&config_path)
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||||
|
|
||||||
|
// Update use_for_backends if specified
|
||||||
|
if req.use_for_backends.is_some() {
|
||||||
|
set_provider_config_entry(
|
||||||
|
&mut opencode_config,
|
||||||
|
provider_type,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
req.use_for_backends.clone(),
|
||||||
|
);
|
||||||
|
if let Err(e) = write_opencode_config(&config_path, &opencode_config) {
|
||||||
|
tracing::error!("Failed to write OpenCode config: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let default_provider = get_default_provider(&opencode_config);
|
let default_provider = get_default_provider(&opencode_config);
|
||||||
let config_entry = get_provider_config_entry(&opencode_config, provider_type);
|
let config_entry = get_provider_config_entry(&opencode_config, provider_type);
|
||||||
let response = build_provider_response(
|
let response = build_provider_response(
|
||||||
|
|||||||
@@ -518,6 +518,7 @@ async fn run_mission_turn(
|
|||||||
events_tx.clone(),
|
events_tx.clone(),
|
||||||
cancel,
|
cancel,
|
||||||
secrets,
|
secrets,
|
||||||
|
&config.working_dir,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -566,21 +567,32 @@ async fn run_claudecode_turn(
|
|||||||
events_tx: broadcast::Sender<AgentEvent>,
|
events_tx: broadcast::Sender<AgentEvent>,
|
||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
secrets: Option<Arc<SecretsStore>>,
|
secrets: Option<Arc<SecretsStore>>,
|
||||||
|
app_working_dir: &std::path::Path,
|
||||||
) -> AgentResult {
|
) -> AgentResult {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use super::ai_providers::get_anthropic_api_key_for_claudecode;
|
||||||
|
|
||||||
// Get API key from secrets
|
// Try to get API key from Anthropic provider configured for Claude Code backend
|
||||||
let api_key = if let Some(ref store) = secrets {
|
let api_key = if let Some(key) = get_anthropic_api_key_for_claudecode(app_working_dir) {
|
||||||
match store.get_secret("claudecode", "api_key").await {
|
tracing::info!("Using Anthropic API key from provider for Claude Code");
|
||||||
Ok(key) => Some(key),
|
Some(key)
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("Failed to get Claude API key from secrets: {}", e);
|
|
||||||
// Fall back to environment variable
|
|
||||||
std::env::var("ANTHROPIC_API_KEY").ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
std::env::var("ANTHROPIC_API_KEY").ok()
|
// Fall back to secrets vault (legacy support)
|
||||||
|
if let Some(ref store) = secrets {
|
||||||
|
match store.get_secret("claudecode", "api_key").await {
|
||||||
|
Ok(key) => {
|
||||||
|
tracing::info!("Using Claude Code API key from secrets vault (legacy)");
|
||||||
|
Some(key)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to get Claude API key from secrets: {}", e);
|
||||||
|
// Fall back to environment variable
|
||||||
|
std::env::var("ANTHROPIC_API_KEY").ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::env::var("ANTHROPIC_API_KEY").ok()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine CLI path
|
// Determine CLI path
|
||||||
|
|||||||
Reference in New Issue
Block a user