Skip to content

Commit f412f06

Browse files
docs(readme): update provider configuration documentation
Add comprehensive documentation for dynamic provider configuration loading from docs repository. Includes: - Details on three-tier fallback strategy (remote → embedded backup → legacy) - Environment variable override options for local development - Embedded backup synchronization instructions - Updated provider list (Anthropic, ZAI, Aliyun, MiniMax, Custom) Co-Authored-By: Hagicode <noreply@hagicode.com>
1 parent dd9bac2 commit f412f06

8 files changed

Lines changed: 851 additions & 143 deletions

File tree

README.md

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,30 +77,63 @@ This will build the application and deploy it to the `gh-pages` branch.
7777
- **Build failures**: Verify dependencies are installed correctly with `npm ci`
7878
- **Permissions**: Ensure the GitHub Actions workflow has the necessary permissions
7979

80-
## Configuration Options
80+
### Configuration Options
8181

82-
### Basic Settings
82+
#### Basic Settings
8383
- **HTTP Port**: Port for the application to listen on
8484
- **Container Name**: Name of the Docker container
8585
- **Image Tag**: Docker image tag to use
8686
- **Host OS**: Target operating system (Windows/Linux)
8787
- **Image Registry**: Docker image registry (Docker Hub/Azure ACR)
8888

89-
### Database
89+
#### Database
9090
- **Internal PostgreSQL**: Built-in PostgreSQL service
9191
- **External Database**: Connect to external PostgreSQL instance
9292
- **Volume Type**: Named volume or bind mount for data storage
9393

94-
### License
94+
#### License
9595
- **Public Test Key**: Default public test license
9696
- **Custom License Key**: Use your own license key
9797

98-
### API Configuration
99-
- **Anthropic**: Official Anthropic API
98+
#### API Configuration
99+
100+
The application dynamically loads provider configurations from the docs repository at `https://docs.hagicode.com/presets/claude-code/providers/`. Available providers include:
101+
102+
- **Anthropic Official**: Official Anthropic API
100103
- **Zhipu AI (ZAI)**: Chinese AI provider with Anthropic-compatible API
104+
- **Aliyun DashScope**: Aliyun's AI service with Anthropic-compatible API
105+
- **MiniMax**: MiniMax AI service with Anthropic-compatible API
101106
- **Custom**: Custom API endpoint with Anthropic-compatible interface
102107

103-
### Volume Mounts
108+
##### Provider Configuration Fallback
109+
110+
The application uses a three-tier fallback strategy for provider configurations:
111+
112+
1. **Primary**: Fetch from docs repository (`https://docs.hagicode.com/presets/claude-code/providers/`)
113+
2. **Fallback**: Use embedded backup configuration (included in code)
114+
3. **Legacy**: Use hardcoded constants (for backward compatibility)
115+
116+
This ensures the application always works, even if the docs repository is temporarily unavailable.
117+
118+
##### Environment Variable Override
119+
120+
For local development, you can override the docs repository URL:
121+
122+
```bash
123+
# Override to use local docs repository
124+
VITE_PRESETS_BASE_URL=http://localhost:3000 npm run dev
125+
126+
# Or specify a different remote URL
127+
VITE_PRESETS_BASE_URL=https://your-custom-docs-url.com npm run dev
128+
```
129+
130+
The default value is `https://docs.hagicode.com`.
131+
132+
##### Embedded Backup Synchronization
133+
134+
The embedded backup configuration (`src/lib/docker-compose/providerConfigLoader.ts`) is synchronized with the docs repository presets. When adding new providers or updating existing ones in the docs repository, update the `EMBEDDED_BACKUP` constant to include the latest data.
135+
136+
#### Volume Mounts
104137
- **Work Directory**: Path to your code repository
105138
- **Root User Warning**: Detection and warning for root-owned directories
106139
- **User Permission Mapping**: PUID/PGID configuration for Linux

src/components/docker-compose/ConfigForm.tsx

Lines changed: 82 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,46 @@
11
import { useSelector, useDispatch } from 'react-redux';
2-
import { selectConfig, setConfigField } from '@/lib/docker-compose/slice';
2+
import { selectConfig, setConfigField, selectProviders, selectProvidersLoading, selectProvidersError, selectProviderById } from '@/lib/docker-compose/slice';
33
import type { DockerComposeConfig, ConfigProfile } from '@/lib/docker-compose/types';
4-
import { REGISTRIES, ZAI_API_URL, ALIYUN_API_URL } from '@/lib/docker-compose/types';
4+
import { REGISTRIES } from '@/lib/docker-compose/types';
5+
import type { ProviderPreset } from '@/lib/docker-compose/providerConfigLoader';
56
import { Label } from '@/components/ui/label';
67
import { Input } from '@/components/ui/input';
78
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
89
import { Checkbox } from '@/components/ui/checkbox';
910
import { Badge } from '@/components/ui/badge';
1011
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
11-
import { Settings } from 'lucide-react';
12+
import { Settings, Loader2, AlertCircle } from 'lucide-react';
1213
import { useTranslation } from 'react-i18next';
1314
import { NAVIGATION_LINKS } from '@/config/navigationLinks';
15+
import { useEffect, useMemo } from 'react';
1416

1517
export function ConfigForm() {
1618
const { t } = useTranslation();
1719
const dispatch = useDispatch();
1820
const config = useSelector(selectConfig);
21+
const providers = useSelector(selectProviders);
22+
const providersLoading = useSelector(selectProvidersLoading);
23+
const providersError = useSelector(selectProvidersError);
1924

2025
const updateConfig = <K extends keyof DockerComposeConfig>(field: K, value: DockerComposeConfig[K]) => {
2126
dispatch(setConfigField({ field, value }));
2227
};
2328

29+
// Get current provider from state
30+
const currentProvider = useMemo(() => {
31+
return selectProviderById({ dockerCompose: { config, providers, isLoading: false, error: null, providersLoading, providersError } }, config.anthropicApiProvider);
32+
}, [config.anthropicApiProvider, providers, providersLoading, providersError]);
33+
34+
// Initialize provider if not set and providers are loaded
35+
useEffect(() => {
36+
if (!providersLoading && providers.length > 0 && (!config.anthropicApiProvider || config.anthropicApiProvider === 'zai')) {
37+
const recommendedProvider = providers.find(p => p.recommended && p.providerId !== 'custom') || providers[0];
38+
if (recommendedProvider && recommendedProvider.providerId !== 'zai') {
39+
updateConfig('anthropicApiProvider', recommendedProvider.providerId);
40+
}
41+
}
42+
}, [providersLoading, providers, config.anthropicApiProvider, updateConfig]);
43+
2444
return (
2545
<div className="space-y-6 p-6 sm:p-8">
2646
{/* Header */}
@@ -377,86 +397,74 @@ export function ConfigForm() {
377397
{t('configForm.unifiedUseOfToken')}
378398
</p>
379399

400+
{/* Provider loading state */}
401+
{providersLoading && (
402+
<div className="flex items-center gap-2 text-sm text-muted-foreground p-3 bg-muted rounded-md">
403+
<Loader2 className="w-4 h-4 animate-spin" />
404+
<span>Loading provider configurations...</span>
405+
</div>
406+
)}
407+
408+
{/* Provider error state */}
409+
{providersError && (
410+
<div className="flex items-start gap-2 text-sm text-red-600 dark:text-red-400 p-3 bg-red-50 dark:bg-red-950/20 rounded-md">
411+
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
412+
<span>Failed to load provider configurations: {providersError}</span>
413+
</div>
414+
)}
415+
380416
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
381417
<div className="space-y-2">
382418
<Label htmlFor="apiProvider">
383419
{t('configForm.apiProvider')} <span className="text-red-500">*</span>
384420
</Label>
385421
<Select
386422
value={config.anthropicApiProvider}
387-
onValueChange={(value: 'anthropic' | 'zai' | 'aliyun' | 'custom') => {
423+
onValueChange={(value: string) => {
388424
updateConfig('anthropicApiProvider', value);
389-
// Auto-set model defaults based on provider
390-
if (value === 'zai') {
391-
// ZAI uses glm models
392-
updateConfig('anthropicSonnetModel', 'glm-4.7');
393-
updateConfig('anthropicOpusModel', 'glm-5');
394-
updateConfig('anthropicHaikuModel', 'glm-4.5-air');
395-
} else if (value === 'aliyun') {
396-
// Aliyun uses unified glm-4.7 model for all tiers
397-
updateConfig('anthropicSonnetModel', 'glm-4.7');
398-
updateConfig('anthropicOpusModel', 'glm-4.7');
399-
updateConfig('anthropicHaikuModel', 'glm-4.7');
400-
} else if (value === 'anthropic') {
401-
// Anthropic uses Claude models (no default needed, user can choose)
402-
// Clear previous provider defaults
403-
if (config.anthropicSonnetModel === 'glm-4.7') {
404-
updateConfig('anthropicSonnetModel', undefined);
425+
// Auto-set model defaults based on provider configuration
426+
const provider = providers.find(p => p.providerId === value);
427+
if (provider && provider.providerId !== 'custom') {
428+
// Apply model defaults from provider configuration
429+
if (provider.defaultModels.sonnet) {
430+
updateConfig('anthropicSonnetModel', provider.defaultModels.sonnet || undefined);
405431
}
406-
if (config.anthropicOpusModel === 'glm-5' || config.anthropicOpusModel === 'qwen3-coder-next' || config.anthropicOpusModel === 'glm-4.7') {
407-
updateConfig('anthropicOpusModel', undefined);
432+
if (provider.defaultModels.opus) {
433+
updateConfig('anthropicOpusModel', provider.defaultModels.opus || undefined);
408434
}
409-
if (config.anthropicHaikuModel === 'glm-4.5-air' || config.anthropicHaikuModel === 'qwen3-coder-plus' || config.anthropicHaikuModel === 'glm-4.7') {
410-
updateConfig('anthropicHaikuModel', undefined);
435+
if (provider.defaultModels.haiku) {
436+
updateConfig('anthropicHaikuModel', provider.defaultModels.haiku || undefined);
411437
}
412438
} else {
413-
// Custom - clear all model defaults
439+
// Custom or no provider defaults - clear model defaults
414440
updateConfig('anthropicSonnetModel', undefined);
415441
updateConfig('anthropicOpusModel', undefined);
416442
updateConfig('anthropicHaikuModel', undefined);
417443
}
418444
}}
445+
disabled={providersLoading || providers.length === 0}
419446
>
420447
<SelectTrigger id="apiProvider">
421448
<SelectValue placeholder={t('configForm.selectApiProvider')} />
422449
</SelectTrigger>
423450
<SelectContent>
424-
<SelectItem value="zai">
425-
<div className="flex items-center gap-2">
426-
<span>{t('configForm.zhipuAI')}</span>
427-
<Badge variant="secondary">{t('common.default')}</Badge>
428-
</div>
429-
</SelectItem>
430-
<SelectItem value="aliyun">
431-
<div className="flex items-center gap-2">
432-
<span>{t('configForm.aliyunDashScope')}</span>
433-
</div>
434-
</SelectItem>
435-
<SelectItem value="anthropic">{t('configForm.anthropicOfficialApi')}</SelectItem>
436-
<SelectItem value="custom">{t('configForm.customApiEndpoint')}</SelectItem>
451+
{providers.map((provider) => (
452+
<SelectItem key={provider.providerId} value={provider.providerId}>
453+
<div className="flex items-center gap-2">
454+
<span>{provider.name}</span>
455+
{provider.recommended && (
456+
<Badge variant="secondary">{t('common.recommended')}</Badge>
457+
)}
458+
</div>
459+
</SelectItem>
460+
))}
437461
</SelectContent>
438462
</Select>
439463

440-
{config.anthropicApiProvider === 'zai' && (
464+
{/* Referral link for current provider */}
465+
{currentProvider && currentProvider.referralUrl && currentProvider.providerId !== 'custom' && (
441466
<a
442-
href="https://www.bigmodel.cn/claude-code?ic=14BY54APZA"
443-
target="_blank"
444-
rel="noopener noreferrer"
445-
className="mt-3 inline-flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 dark:from-purple-500 dark:to-blue-500 dark:hover:from-purple-600 dark:hover:to-blue-600 text-white text-sm font-medium rounded-lg shadow-md hover:shadow-lg transition-all duration-200 group"
446-
>
447-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
448-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
449-
</svg>
450-
<span>{t('configForm.getApiToken')}</span>
451-
<svg className="w-4 h-4 opacity-70 group-hover:translate-x-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
452-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
453-
</svg>
454-
</a>
455-
)}
456-
457-
{config.anthropicApiProvider === 'aliyun' && (
458-
<a
459-
href="https://www.aliyun.com/benefit/ai/aistar?userCode=vmx5szbq&clubBiz=subTask..12384055..10263.."
467+
href={currentProvider.referralUrl}
460468
target="_blank"
461469
rel="noopener noreferrer"
462470
className="mt-3 inline-flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 dark:from-purple-500 dark:to-blue-500 dark:hover:from-purple-600 dark:hover:to-blue-600 text-white text-sm font-medium rounded-lg shadow-md hover:shadow-lg transition-all duration-200 group"
@@ -473,6 +481,7 @@ export function ConfigForm() {
473481
</div>
474482

475483
<div className="space-y-2">
484+
{/* Custom API URL input - only show for custom provider */}
476485
{config.anthropicApiProvider === 'custom' && (
477486
<>
478487
<Label htmlFor="anthropicUrl">{t('configForm.apiEndpointUrl')}</Label>
@@ -497,30 +506,28 @@ export function ConfigForm() {
497506
value={config.anthropicAuthToken}
498507
onChange={(e) => updateConfig('anthropicAuthToken', e.target.value)}
499508
placeholder={
500-
config.anthropicApiProvider === 'anthropic'
509+
currentProvider?.providerId === 'anthropic'
501510
? t('configForm.enterAnthropicApiToken')
502511
: t('configForm.enterApiToken')
503512
}
504513
/>
505514

506-
{config.anthropicApiProvider === 'zai' && (
507-
<div className="mt-2 p-3 bg-green-50 dark:bg-green-950 rounded-md text-sm">
508-
<p>{t('configForm.apiEndpointAutoSet')}: {ZAI_API_URL}</p>
509-
</div>
510-
)}
511-
512-
{config.anthropicApiProvider === 'aliyun' && (
515+
{/* Provider-specific information */}
516+
{currentProvider && config.anthropicApiProvider !== 'custom' && (
513517
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-950 rounded-md text-sm">
514-
<p>{t('configForm.apiEndpointAutoSet')}: {ALIYUN_API_URL}</p>
515-
<p className="text-xs text-muted-foreground mt-1">
516-
{t('configForm.aliyunModelsSupported')}: glm-4.7 (unified model for all tiers: Haiku, Sonnet, Opus)
517-
</p>
518-
</div>
519-
)}
520-
521-
{config.anthropicApiProvider === 'anthropic' && (
522-
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-950 rounded-md text-sm">
523-
<p>{t('configForm.usingOfficialApi')}</p>
518+
{currentProvider.providerId === 'anthropic' ? (
519+
<p>{t('configForm.usingOfficialApi')}</p>
520+
) : (
521+
<>
522+
<p>{t('configForm.apiEndpointAutoSet')}: {currentProvider.apiUrl.codingPlanForAnthropic}</p>
523+
{currentProvider.description && (
524+
<p className="text-xs text-muted-foreground mt-1">{currentProvider.description}</p>
525+
)}
526+
{currentProvider.notes && (
527+
<p className="text-xs text-muted-foreground mt-1">{currentProvider.notes}</p>
528+
)}
529+
</>
530+
)}
524531
</div>
525532
)}
526533

src/components/docker-compose/ConfigPreview.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useMemo } from 'react';
22
import { useSelector } from 'react-redux';
3-
import { selectConfig } from '@/lib/docker-compose/slice';
3+
import { selectConfig, selectProviderById } from '@/lib/docker-compose/slice';
44
import { generateYAML } from '@/lib/docker-compose/generator';
55
import { SyntaxHighlighter } from '@/components/ui/syntax-highlighter';
66
import { Button } from '@/components/ui/button';
@@ -15,7 +15,10 @@ export function ConfigPreview() {
1515
const config = useSelector(selectConfig);
1616
const [copied, setCopied] = useState(false);
1717

18-
const yaml = useMemo(() => generateYAML(config, i18n.language), [config, i18n.language]);
18+
// Get provider configuration for YAML generation
19+
const providerConfig = useSelector((state: any) => selectProviderById(state, config.anthropicApiProvider));
20+
21+
const yaml = useMemo(() => generateYAML(config, providerConfig, i18n.language), [config, providerConfig, i18n.language]);
1922
const darkMode = theme === 'dark';
2023

2124
const handleCopy = async () => {

0 commit comments

Comments
 (0)