Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions cmd/simple-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/nachoal/simple-agent-go/llm/anthropic"
"github.com/nachoal/simple-agent-go/llm/deepseek"
"github.com/nachoal/simple-agent-go/llm/groq"
"github.com/nachoal/simple-agent-go/llm/llamacpp"
"github.com/nachoal/simple-agent-go/llm/lmstudio"
"github.com/nachoal/simple-agent-go/llm/moonshot"
"github.com/nachoal/simple-agent-go/llm/ollama"
Expand Down Expand Up @@ -158,7 +159,7 @@ func runTUI(cmd *cobra.Command, args []string) error {

// Create all provider clients for model selection
providers := make(map[string]llm.Client)
providerNames := []string{"openai", "anthropic", "moonshot", "deepseek", "perplexity", "groq", "lmstudio", "ollama"}
providerNames := []string{"openai", "anthropic", "moonshot", "deepseek", "perplexity", "groq", "lmstudio", "ollama", "llamacpp"}

// Debug: count successful providers
successCount := 0
Expand All @@ -185,10 +186,14 @@ func runTUI(cmd *cobra.Command, args []string) error {
}

// Create agent
agentInstance := agent.New(llmClient,
agent.WithMaxIterations(10),
agent.WithTemperature(0.7),
)
var agentOpts []agent.Option
agentOpts = append(agentOpts, agent.WithMaxIterations(10))
agentOpts = append(agentOpts, agent.WithTemperature(0.7))

// GPT-OSS models now supported with Harmony parser
// Tools are enabled automatically via the Harmony format parser in lmstudio/harmony.go

agentInstance := agent.New(llmClient, agentOpts...)

// Initialize history manager
historyMgr, err := history.NewManager()
Expand Down Expand Up @@ -228,10 +233,12 @@ func runTUI(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("failed to create %s client: %w", provider, err)
}
agentInstance = agent.New(llmClient,
agent.WithMaxIterations(10),
agent.WithTemperature(0.7),
)
// Recreate agent with proper options
var opts []agent.Option
opts = append(opts, agent.WithMaxIterations(10))
opts = append(opts, agent.WithTemperature(0.7))
// GPT-OSS models now supported with Harmony parser
agentInstance = agent.New(llmClient, opts...)
}
}
} else if resumeSet {
Expand Down Expand Up @@ -314,10 +321,12 @@ func runTUI(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("failed to create %s client: %w", provider, err)
}
agentInstance = agent.New(llmClient,
agent.WithMaxIterations(10),
agent.WithTemperature(0.7),
)
// Recreate agent with proper options
var opts []agent.Option
opts = append(opts, agent.WithMaxIterations(10))
opts = append(opts, agent.WithTemperature(0.7))
// GPT-OSS models now supported with Harmony parser
agentInstance = agent.New(llmClient, opts...)
}
} else {
// Start new session
Expand Down Expand Up @@ -399,10 +408,14 @@ func runQuery(cmd *cobra.Command, args []string) error {
defer llmClient.Close()

// Create agent
agentInstance := agent.New(llmClient,
agent.WithMaxIterations(10),
agent.WithTemperature(0.7),
)
var agentOpts []agent.Option
agentOpts = append(agentOpts, agent.WithMaxIterations(10))
agentOpts = append(agentOpts, agent.WithTemperature(0.7))

// GPT-OSS models now supported with Harmony parser
// Tools are enabled automatically via the Harmony format parser in lmstudio/harmony.go

agentInstance := agent.New(llmClient, agentOpts...)

// If verbose, show the enhanced system prompt (including tools)
if verbose {
Expand Down Expand Up @@ -507,6 +520,9 @@ func createLLMClient(provider, model string) (llm.Client, error) {
case "ollama":
return ollama.NewClient(llm.WithModel(model))

case "llamacpp", "llama.cpp", "llama-cpp":
return llamacpp.NewClient(llm.WithModel(model))

default:
return nil, fmt.Errorf("unknown provider: %s", provider)
}
Expand All @@ -522,6 +538,7 @@ func getDefaultModel(provider string) string {
"groq": "mixtral-8x7b-32768",
"lmstudio": "local-model",
"ollama": "llama2",
"llamacpp": "gpt-oss-120b",
}

if model, ok := defaults[strings.ToLower(provider)]; ok {
Expand Down
135 changes: 135 additions & 0 deletions docs/GPT-OSS-HARMONY-ISSUE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# GPT-OSS Harmony Format Issue & Solution

## The Problem

GPT-OSS models output responses in OpenAI's Harmony format, which contains channel tags that break llama.cpp's chat parser:

```
<|channel|>analysis<|message|>Thinking about the problem...<|start|>assistant<|channel|>final<|message|>Here's the answer
```

When llama.cpp encounters `<|start|>assistant` within a response, it crashes with:
```
libc++abi: terminating due to uncaught exception of type std::runtime_error: Unexpected content at end of input
```

## Current Status

1. **llama.cpp PR #15091** adds GPT-OSS support but has known template parsing issues
2. **Community workarounds** (no `--jinja` flag) produce raw Harmony output, unusable for chat UIs
3. **"Fixed" templates** only fix input formatting, not output parsing
4. **Server crashes** in TUI mode when GPT-OSS outputs Harmony format

## Proposed Solution: Harmony Parser in simple-agent

Add a Harmony format parser to simple-agent-go that:

1. **Detects GPT-OSS models** (when provider is "llamacpp" and model contains "gpt-oss")
2. **Intercepts responses** before standard parsing
3. **Extracts channels**:
- `<|channel|>analysis<|message|>` → Reasoning/thinking content
- `<|channel|>final<|message|>` → User-visible response
- `<|channel|>commentary<|message|>` → Tool calls
4. **Converts to standard format** that simple-agent expects

## Implementation Steps

### 1. Create Harmony Parser (`llm/llamacpp/harmony.go`)
```go
package llamacpp

import (
"regexp"
"strings"
)

type HarmonyResponse struct {
Analysis string
Final string
Commentary string
ToolCalls []ToolCall
}

func ParseHarmonyFormat(content string) (*HarmonyResponse, error) {
// Extract channel contents with regex
analysisRe := regexp.MustCompile(`<\|channel\|>analysis<\|message\|>(.*?)(?:<\|start\|>|<\|end\|>|$)`)
finalRe := regexp.MustCompile(`<\|channel\|>final<\|message\|>(.*?)(?:<\|return\|>|<\|end\|>|$)`)
commentaryRe := regexp.MustCompile(`<\|channel\|>commentary.*?<\|message\|>(.*?)(?:<\|call\|>|<\|end\|>|$)`)

// Parse and return structured response
}

func ConvertToStandardResponse(harmony *HarmonyResponse) string {
// Return clean final content for display
return harmony.Final
}
```

### 2. Modify llamacpp Client (`llm/llamacpp/client.go`)

In the `Chat` method, add Harmony detection:
```go
func (c *Client) Chat(ctx context.Context, request *llm.ChatRequest) (*llm.ChatResponse, error) {
// ... existing code ...

// After getting response
if strings.Contains(c.model, "gpt-oss") && strings.Contains(chatResp.Choices[0].Message.Content, "<|channel|>") {
harmony, err := ParseHarmonyFormat(chatResp.Choices[0].Message.Content)
if err == nil {
// Convert Harmony format to standard response
chatResp.Choices[0].Message.Content = harmony.Final
// Store reasoning if field exists
// Convert commentary to tool calls if present
}
}

return &chatResp, nil
}
```

### 3. Enable Tools for GPT-OSS

Once Harmony parsing works, re-enable tools in `cmd/simple-agent/main.go`:
```go
// Remove the tool disable for GPT-OSS
// The Harmony parser will handle tool calls from commentary channel
```

## Testing

1. Start server without `--jinja` to get raw Harmony output:
```bash
~/bin/llama-cpp-latest/llama-server \
--model ~/.cache/lm-studio/models/ggml-org/gpt-oss-120b-GGUF/gpt-oss-120b-mxfp4-00001-of-00003.gguf \
--host 0.0.0.0 --port 8080 -c 8192 -fa
```

2. Test with simple-agent:
```bash
./simple-agent --provider llamacpp --model gpt-oss-120b
```

## Files to Modify

1. `/Users/ia/code/projects/simple-agents/simple-agent-go/llm/llamacpp/harmony.go` (NEW)
2. `/Users/ia/code/projects/simple-agents/simple-agent-go/llm/llamacpp/client.go` (MODIFY Chat/ChatStream)
3. `/Users/ia/code/projects/simple-agents/simple-agent-go/cmd/simple-agent/main.go` (RE-ENABLE tools)

## Expected Outcome

- GPT-OSS works in TUI mode without crashes
- Clean responses without visible Harmony tags
- Tool calling support through commentary channel parsing
- Reasoning available in separate field

## Alternative Approaches

1. **Wait for llama.cpp fix** - PR #15091 discussion suggests ongoing work
2. **Use different model** - Other models work fine with current setup
3. **Post-process in TUI** - Less clean but isolates changes to UI layer

## References

- llama.cpp PR #15091: https://github.com/ggml-org/llama.cpp/pull/15091
- OpenAI Harmony: https://github.com/openai/harmony
- Fixed template attempt: https://huggingface.co/DevQuasar/openai.gpt-oss-20b-GGUF/resolve/main/gpt-oss_fixed.jinja
52 changes: 51 additions & 1 deletion llm/lmstudio/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,60 @@ func (c *Client) Chat(ctx context.Context, request *llm.ChatRequest) (*llm.ChatR
return nil, fmt.Errorf("failed to parse response: %w", err)
}

// Check if this is a GPT-OSS model with Harmony format
if len(response.Choices) > 0 && response.Choices[0].Message.Content != nil {
content := *response.Choices[0].Message.Content

// Detect GPT-OSS model and Harmony format
isGPTOSS := strings.Contains(strings.ToLower(request.Model), "gpt-oss") ||
strings.Contains(strings.ToLower(request.Model), "gpt_oss")

if isGPTOSS && IsHarmonyFormat(content) {
if os.Getenv("SIMPLE_AGENT_DEBUG") == "true" {
fmt.Fprintf(os.Stderr, "[LM Studio] Detected Harmony format in GPT-OSS response\n")
fmt.Fprintf(os.Stderr, "[LM Studio] Raw content: %s\n", content)
}

// Parse Harmony format
harmony, err := ParseHarmonyFormat(content)
if err == nil {
// Replace content with clean final message
cleanContent := ConvertToStandardResponse(harmony)
// Only set content if there's actual final content, otherwise set to nil for tool calls
if cleanContent != "" || len(harmony.ToolCalls) == 0 {
response.Choices[0].Message.Content = &cleanContent
} else {
// Set content to nil when there are only tool calls (no final message)
response.Choices[0].Message.Content = nil
}

// Add tool calls if present
if len(harmony.ToolCalls) > 0 {
response.Choices[0].Message.ToolCalls = harmony.ToolCalls

if os.Getenv("SIMPLE_AGENT_DEBUG") == "true" {
fmt.Fprintf(os.Stderr, "[LM Studio] Extracted %d tool calls from Harmony format\n", len(harmony.ToolCalls))
for i, tc := range harmony.ToolCalls {
fmt.Fprintf(os.Stderr, "[LM Studio] Tool Call %d: %s with args: %s\n",
i, tc.Function.Name, string(tc.Function.Arguments))
}
}
}

// Store analysis/reasoning if needed (could be added to a custom field later)
if os.Getenv("SIMPLE_AGENT_DEBUG") == "true" && harmony.Analysis != "" {
fmt.Fprintf(os.Stderr, "[LM Studio] Analysis channel: %s\n", harmony.Analysis)
}
} else if os.Getenv("SIMPLE_AGENT_DEBUG") == "true" {
fmt.Fprintf(os.Stderr, "[LM Studio] Failed to parse Harmony format: %v\n", err)
}
}
}

// Debug log parsed response
if os.Getenv("SIMPLE_AGENT_DEBUG") == "true" {
if len(response.Choices) > 0 && len(response.Choices[0].Message.ToolCalls) > 0 {
fmt.Fprintf(os.Stderr, "[LM Studio] Parsed %d tool calls\n", len(response.Choices[0].Message.ToolCalls))
fmt.Fprintf(os.Stderr, "[LM Studio] Final parsed %d tool calls\n", len(response.Choices[0].Message.ToolCalls))
for i, tc := range response.Choices[0].Message.ToolCalls {
fmt.Fprintf(os.Stderr, "[LM Studio] Tool Call %d: %s with args: %s\n", i, tc.Function.Name, string(tc.Function.Arguments))
}
Expand Down
Loading