// Package llm provides FakeLLMProvider — a zero-dependency LLM provider that // returns canned responses for ChatCompletion, embeddings, and streaming. // // Usage: set FAKE_AI_ENABLED=true in the environment to activate; it overrides // the standard provider manager so Captain/Copilot endpoints respond without a // real LLM API key. package llm import ( "context" "fmt" "os" "sync/atomic" ) // FakeLLMProvider implements Provider with canned responses. // Useful for integration testing, staging, and demo environments. type FakeLLMProvider struct { // CallCount tracks how many ChatCompletion calls were made. callCount atomic.Int64 } // NewFakeLLMProvider creates a new FakeLLMProvider. // It is only active when the FAKE_AI_ENABLED env var is set to "true". func NewFakeLLMProvider() *FakeLLMProvider { return &FakeLLMProvider{} } // IsFakeAIEnabled returns true when the environment variable FAKE_AI_ENABLED=true. func IsFakeAIEnabled() bool { return os.Getenv("FAKE_AI_ENABLED") == "true" } // ChatCompletion returns a canned response echoing the last user message. func (f *FakeLLMProvider) ChatCompletion(_ context.Context, req ChatRequest) (*ChatResponse, error) { n := f.callCount.Add(1) // Build a reply from the last user message. userMsg := "" for i := len(req.Messages) - 1; i >= 0; i-- { if req.Messages[i].Role == "user" { userMsg = req.Messages[i].Content break } } if userMsg == "" { userMsg = "Hello! I'm a fake AI assistant. How can I help you today?" } reply := fmt.Sprintf("[FakeAI #%d] Received your message. Here is a simulated response: %s", n, userMsg) return &ChatResponse{ ID: "fake-" + fmt.Sprint(n), Object: "chat.completion", Created: 1700000000, Model: "fake-ai-model", Choices: []ChatChoice{ { Index: 0, Message: ChatMessage{ Role: "assistant", Content: reply, }, FinishReason: "stop", }, }, Usage: TokenUsage{ PromptTokens: 10, CompletionTokens: 5, TotalTokens: 15, }, }, nil } // CreateEmbedding returns a zero-vector embedding of dimension 384. func (f *FakeLLMProvider) CreateEmbedding(_ context.Context, req EmbeddingRequest) (*EmbeddingResponse, error) { if len(req.Input) == 0 { return &EmbeddingResponse{ Object: "list", Data: []EmbeddingData{}, Model: "fake-embedding-model", }, nil } dim := req.Dimensions if dim <= 0 { dim = 384 } embeddings := make([]EmbeddingData, len(req.Input)) for i := range req.Input { embeddings[i] = EmbeddingData{ Object: "embedding", Index: i, Embedding: make([]float64, dim), } } return &EmbeddingResponse{ Object: "list", Data: embeddings, Model: "fake-embedding-model", Usage: TokenUsage{ PromptTokens: len(req.Input), CompletionTokens: 0, TotalTokens: len(req.Input), }, }, nil } // ChatCompletionStream simulates streaming by delivering a single chunk. func (f *FakeLLMProvider) ChatCompletionStream(ctx context.Context, req ChatRequest, onChunk func(StreamChunk) error) error { resp, err := f.ChatCompletion(ctx, req) if err != nil { return err } if len(resp.Choices) == 0 { return nil } chunk := StreamChunk{ ID: resp.ID, Object: "chat.completion.chunk", Created: resp.Created, Model: resp.Model, Choices: []StreamChoice{ { Index: 0, Delta: StreamDelta{ Role: "assistant", Content: resp.Choices[0].Message.Content, }, FinishReason: "stop", }, }, } return onChunk(chunk) }