package ai import ( "context" "fmt" "strings" ) // Client is AI provider transport (real HTTP or fake). type Client interface { Complete(ctx context.Context, apiKey, model, prompt string) (string, error) // CompleteStream 串流 delta;onDelta 可為 nil。回傳完整正文。 CompleteStream(ctx context.Context, apiKey, model, prompt string, onDelta func(chunk string) error) (string, error) ListModels(ctx context.Context, apiKey string) ([]string, error) } // FakeClient for tests / when no real key path needed. type FakeClient struct { Fail bool Models []string } func (f *FakeClient) Complete(ctx context.Context, apiKey, model, prompt string) (string, error) { if f.Fail || apiKey == "" { return "", fmt.Errorf("ai call failed: no key or forced fail") } _ = SystemPromptForLanguage(ResponseLanguageFrom(ctx)) // ensure always defined for real clients if len(prompt) > 200 { prompt = prompt[:200] } return fmt.Sprintf("[%s] %s", model, strings.TrimSpace(prompt)), nil } func (f *FakeClient) ListModels(_ context.Context, apiKey string) ([]string, error) { if f.Fail || apiKey == "" { return nil, fmt.Errorf("list models failed: no key or forced fail") } if len(f.Models) > 0 { return append([]string(nil), f.Models...), nil } return []string{"grok-3", "grok-2", "grok-beta"}, nil } // ModelsForProvider — static catalog (fallback when live list fails). func ModelsForProvider(provider string) []string { return FallbackModels(provider) }