package llmfs import ( "fmt" "io" "strconv" "strings" "github.com/NERVsystems/llm9p/internal/llm" "github.com/NERVsystems/llm9p/internal/protocol" ) // SessionModelFile controls the model for a session: /n/llm/N/model type SessionModelFile struct { *protocol.BaseFile sm *llm.SessionManager id int } // NewSessionModelFile creates a model file for the given session. func NewSessionModelFile(sm *llm.SessionManager, id int) *SessionModelFile { return &SessionModelFile{ BaseFile: protocol.NewBaseFile("model", 0666), sm: sm, id: id, } } // Read returns the current model name. func (f *SessionModelFile) Read(p []byte, offset int64) (int, error) { session := f.sm.Get(f.id) if session == nil { return 0, protocol.ErrNotFound } content := session.Model() + "\n" if offset >= int64(len(content)) { return 0, io.EOF } return copy(p, content[offset:]), nil } // modelAliases maps short convenience names to full Anthropic model IDs. var modelAliases = map[string]string{ "haiku": "claude-haiku-4-5-20251001", "sonnet": "claude-sonnet-4-6", "opus": "claude-opus-4-6", } // resolveModel expands a short alias to its full model ID, or returns the // input unchanged if it is already a full ID (or unknown). func resolveModel(name string) string { if full, ok := modelAliases[strings.ToLower(name)]; ok { return full } return name } // Write sets the model name. func (f *SessionModelFile) Write(p []byte, offset int64) (int, error) { session := f.sm.Get(f.id) if session == nil { return 0, protocol.ErrNotFound } model := resolveModel(strings.TrimSpace(string(p))) if model != "" { session.SetModel(model) } return len(p), nil } // Stat returns the file's metadata. func (f *SessionModelFile) Stat() protocol.Stat { s := f.BaseFile.Stat() session := f.sm.Get(f.id) if session != nil { s.Length = uint64(len(session.Model()) + 1) } return s } // SessionTemperatureFile controls the temperature for a session: /n/llm/N/temperature type SessionTemperatureFile struct { *protocol.BaseFile sm *llm.SessionManager id int } // NewSessionTemperatureFile creates a temperature file for the given session. func NewSessionTemperatureFile(sm *llm.SessionManager, id int) *SessionTemperatureFile { return &SessionTemperatureFile{ BaseFile: protocol.NewBaseFile("temperature", 0666), sm: sm, id: id, } } // Read returns the current temperature. func (f *SessionTemperatureFile) Read(p []byte, offset int64) (int, error) { session := f.sm.Get(f.id) if session == nil { return 0, protocol.ErrNotFound } content := fmt.Sprintf("%.2f\n", session.Temperature()) if offset >= int64(len(content)) { return 0, io.EOF } return copy(p, content[offset:]), nil } // Write sets the temperature. func (f *SessionTemperatureFile) Write(p []byte, offset int64) (int, error) { session := f.sm.Get(f.id) if session == nil { return 0, protocol.ErrNotFound } temp, err := strconv.ParseFloat(strings.TrimSpace(string(p)), 64) if err != nil { return 0, protocol.Error("invalid temperature: " + err.Error()) } if temp < 0.0 || temp > 2.0 { return 0, protocol.Error("temperature must be between 0.0 and 2.0") } session.SetTemperature(temp) return len(p), nil } // Stat returns the file's metadata. func (f *SessionTemperatureFile) Stat() protocol.Stat { s := f.BaseFile.Stat() session := f.sm.Get(f.id) if session != nil { s.Length = uint64(len(fmt.Sprintf("%.2f\n", session.Temperature()))) } return s } // SessionSystemFile controls the system prompt for a session: /n/llm/N/system type SessionSystemFile struct { *protocol.BaseFile sm *llm.SessionManager id int } // NewSessionSystemFile creates a system file for the given session. func NewSessionSystemFile(sm *llm.SessionManager, id int) *SessionSystemFile { return &SessionSystemFile{ BaseFile: protocol.NewBaseFile("system", 0666), sm: sm, id: id, } } // Read returns the current system prompt. func (f *SessionSystemFile) Read(p []byte, offset int64) (int, error) { session := f.sm.Get(f.id) if session == nil { return 0, protocol.ErrNotFound } content := session.SystemPrompt() if content != "" && !strings.HasSuffix(content, "\n") { content += "\n" } if offset >= int64(len(content)) { return 0, io.EOF } return copy(p, content[offset:]), nil } // Write sets the system prompt. func (f *SessionSystemFile) Write(p []byte, offset int64) (int, error) { session := f.sm.Get(f.id) if session == nil { return 0, protocol.ErrNotFound } prompt := strings.TrimSpace(string(p)) session.SetSystemPrompt(prompt) return len(p), nil } // Stat returns the file's metadata. func (f *SessionSystemFile) Stat() protocol.Stat { s := f.BaseFile.Stat() session := f.sm.Get(f.id) if session != nil { content := session.SystemPrompt() if content != "" { s.Length = uint64(len(content) + 1) } } return s } // SessionThinkingFile controls the thinking token budget: /n/llm/N/thinking type SessionThinkingFile struct { *protocol.BaseFile sm *llm.SessionManager id int } // NewSessionThinkingFile creates a thinking file for the given session. func NewSessionThinkingFile(sm *llm.SessionManager, id int) *SessionThinkingFile { return &SessionThinkingFile{ BaseFile: protocol.NewBaseFile("thinking", 0666), sm: sm, id: id, } } // Read returns the current thinking token budget. func (f *SessionThinkingFile) Read(p []byte, offset int64) (int, error) { session := f.sm.Get(f.id) if session == nil { return 0, protocol.ErrNotFound } tokens := session.ThinkingTokens() var content string switch { case tokens < 0: content = "max\n" case tokens == 0: content = "disabled\n" default: content = fmt.Sprintf("%d\n", tokens) } if offset >= int64(len(content)) { return 0, io.EOF } return copy(p, content[offset:]), nil } // Write sets the thinking token budget. func (f *SessionThinkingFile) Write(p []byte, offset int64) (int, error) { session := f.sm.Get(f.id) if session == nil { return 0, protocol.ErrNotFound } value := strings.TrimSpace(string(p)) switch value { case "max", "-1": session.SetThinkingTokens(-1) case "disabled", "off", "0": session.SetThinkingTokens(0) default: tokens, err := strconv.Atoi(value) if err != nil { return 0, protocol.Error("invalid thinking budget: " + err.Error()) } session.SetThinkingTokens(tokens) } return len(p), nil } // Stat returns the file's metadata. func (f *SessionThinkingFile) Stat() protocol.Stat { s := f.BaseFile.Stat() // Estimate length s.Length = 16 return s } // SessionPrefillFile controls the response prefill: /n/llm/N/prefill type SessionPrefillFile struct { *protocol.BaseFile sm *llm.SessionManager id int } // NewSessionPrefillFile creates a prefill file for the given session. func NewSessionPrefillFile(sm *llm.SessionManager, id int) *SessionPrefillFile { return &SessionPrefillFile{ BaseFile: protocol.NewBaseFile("prefill", 0666), sm: sm, id: id, } } // Read returns the current prefill string. func (f *SessionPrefillFile) Read(p []byte, offset int64) (int, error) { session := f.sm.Get(f.id) if session == nil { return 0, protocol.ErrNotFound } content := session.Prefill() if content != "" && !strings.HasSuffix(content, "\n") { content += "\n" } if offset >= int64(len(content)) { return 0, io.EOF } return copy(p, content[offset:]), nil } // Write sets the prefill string. func (f *SessionPrefillFile) Write(p []byte, offset int64) (int, error) { session := f.sm.Get(f.id) if session == nil { return 0, protocol.ErrNotFound } // Don't trim - prefill may have intentional trailing space prefill := string(p) // But do remove trailing newline since shell adds it prefill = strings.TrimSuffix(prefill, "\n") session.SetPrefill(prefill) return len(p), nil } // Stat returns the file's metadata. func (f *SessionPrefillFile) Stat() protocol.Stat { s := f.BaseFile.Stat() session := f.sm.Get(f.id) if session != nil { content := session.Prefill() if content != "" { s.Length = uint64(len(content) + 1) } } return s }