cmd/run: add thinkingstderr argument flag

This commit is contained in:
Pedro Lira 2026-04-16 16:53:42 -03:00
parent b9cb535407
commit a4654f9b2f
No known key found for this signature in database
GPG key ID: 601C9C820AF5A1B6
3 changed files with 107 additions and 49 deletions

View file

@ -596,6 +596,12 @@ func RunHandler(cmd *cobra.Command, args []string) error {
}
opts.HideThinking = hidethinking
thinkingstderr, err := cmd.Flags().GetBool("thinkingstderr")
if err != nil {
return err
}
opts.ThinkingToStderr = thinkingstderr
keepAlive, err := cmd.Flags().GetString("keepalive")
if err != nil {
return err
@ -761,7 +767,7 @@ func RunHandler(cmd *cobra.Command, args []string) error {
// Use experimental agent loop with tools
if isExperimental {
return xcmd.GenerateInteractive(cmd, opts.Model, opts.WordWrap, opts.Options, opts.Think, opts.HideThinking, opts.KeepAlive, yoloMode, enableWebsearch)
return xcmd.GenerateInteractive(cmd, opts.Model, opts.WordWrap, opts.Options, opts.Think, opts.HideThinking, opts.ThinkingToStderr, opts.KeepAlive, yoloMode, enableWebsearch)
}
return generateInteractive(cmd, opts)
@ -1424,8 +1430,9 @@ type runOptions struct {
MultiModal bool
KeepAlive *api.Duration
Think *api.ThinkValue
HideThinking bool
ShowConnect bool
HideThinking bool
ThinkingToStderr bool
ShowConnect bool
}
func (r runOptions) Copy() runOptions {
@ -1475,8 +1482,9 @@ func (r runOptions) Copy() runOptions {
MultiModal: r.MultiModal,
KeepAlive: r.KeepAlive,
Think: think,
HideThinking: r.HideThinking,
ShowConnect: r.ShowConnect,
HideThinking: r.HideThinking,
ThinkingToStderr: r.ThinkingToStderr,
ShowConnect: r.ShowConnect,
}
}
@ -1491,7 +1499,11 @@ type displayResponseState struct {
}
func displayResponse(content string, wordWrap bool, state *displayResponseState) {
termWidth, _, _ := term.GetSize(int(os.Stdout.Fd()))
displayResponseTo(os.Stdout, int(os.Stdout.Fd()), content, wordWrap, state)
}
func displayResponseTo(w io.Writer, fd int, content string, wordWrap bool, state *displayResponseState) {
termWidth, _, _ := term.GetSize(fd)
if termWidth == 0 {
termWidth = 80
}
@ -1499,7 +1511,7 @@ func displayResponse(content string, wordWrap bool, state *displayResponseState)
for _, ch := range content {
if state.lineLength+1 > termWidth-5 {
if runewidth.StringWidth(state.wordBuffer) > termWidth-10 {
fmt.Printf("%s%c", state.wordBuffer, ch)
fmt.Fprintf(w, "%s%c", state.wordBuffer, ch)
state.wordBuffer = ""
state.lineLength = 0
continue
@ -1508,15 +1520,15 @@ func displayResponse(content string, wordWrap bool, state *displayResponseState)
// backtrack the length of the last word and clear to the end of the line
a := runewidth.StringWidth(state.wordBuffer)
if a > 0 {
fmt.Printf("\x1b[%dD", a)
fmt.Fprintf(w, "\x1b[%dD", a)
}
fmt.Printf("\x1b[K\n")
fmt.Printf("%s%c", state.wordBuffer, ch)
fmt.Fprintf(w, "\x1b[K\n")
fmt.Fprintf(w, "%s%c", state.wordBuffer, ch)
chWidth := runewidth.RuneWidth(ch)
state.lineLength = runewidth.StringWidth(state.wordBuffer) + chWidth
} else {
fmt.Print(string(ch))
fmt.Fprint(w, string(ch))
state.lineLength += runewidth.RuneWidth(ch)
if runewidth.RuneWidth(ch) >= 2 {
state.wordBuffer = ""
@ -1535,7 +1547,7 @@ func displayResponse(content string, wordWrap bool, state *displayResponseState)
}
}
} else {
fmt.Printf("%s%s", state.wordBuffer, content)
fmt.Fprintf(w, "%s%s", state.wordBuffer, content)
if len(state.wordBuffer) > 0 {
state.wordBuffer = ""
}
@ -1592,32 +1604,42 @@ func chat(cmd *cobra.Command, opts runOptions) (*api.Message, error) {
var thinkTagOpened bool = false
var thinkTagClosed bool = false
showThinking := !opts.HideThinking || opts.ThinkingToStderr
thinkWriter := io.Writer(os.Stdout)
thinkFd := int(os.Stdout.Fd())
thinkPlainText := false
if opts.ThinkingToStderr {
thinkWriter = os.Stderr
thinkFd = int(os.Stderr.Fd())
thinkPlainText = !term.IsTerminal(thinkFd)
}
role := "assistant"
fn := func(response api.ChatResponse) error {
if response.Message.Content != "" || !opts.HideThinking {
if response.Message.Content != "" || showThinking {
p.StopAndClear()
}
latest = response
role = response.Message.Role
if response.Message.Thinking != "" && !opts.HideThinking {
if response.Message.Thinking != "" && showThinking {
if !thinkTagOpened {
fmt.Print(thinkingOutputOpeningText(false))
fmt.Fprint(thinkWriter, thinkingOutputOpeningText(thinkPlainText))
thinkTagOpened = true
thinkTagClosed = false
}
thinkingContent.WriteString(response.Message.Thinking)
displayResponse(response.Message.Thinking, opts.WordWrap, state)
displayResponseTo(thinkWriter, thinkFd, response.Message.Thinking, opts.WordWrap, state)
}
content := response.Message.Content
if thinkTagOpened && !thinkTagClosed && (content != "" || len(response.Message.ToolCalls) > 0) {
if !strings.HasSuffix(thinkingContent.String(), "\n") {
fmt.Println()
fmt.Fprintln(thinkWriter)
}
fmt.Print(thinkingOutputClosingText(false))
fmt.Fprint(thinkWriter, thinkingOutputClosingText(thinkPlainText))
thinkTagOpened = false
thinkTagClosed = true
state = &displayResponseState{}
@ -1725,29 +1747,39 @@ func generate(cmd *cobra.Command, opts runOptions) error {
plainText := !term.IsTerminal(int(os.Stdout.Fd()))
showThinking := !opts.HideThinking || opts.ThinkingToStderr
thinkWriter := io.Writer(os.Stdout)
thinkFd := int(os.Stdout.Fd())
thinkPlainText := plainText
if opts.ThinkingToStderr {
thinkWriter = os.Stderr
thinkFd = int(os.Stderr.Fd())
thinkPlainText = !term.IsTerminal(thinkFd)
}
fn := func(response api.GenerateResponse) error {
latest = response
content := response.Response
if response.Response != "" || !opts.HideThinking {
if response.Response != "" || showThinking {
p.StopAndClear()
}
if response.Thinking != "" && !opts.HideThinking {
if response.Thinking != "" && showThinking {
if !thinkTagOpened {
fmt.Print(thinkingOutputOpeningText(plainText))
fmt.Fprint(thinkWriter, thinkingOutputOpeningText(thinkPlainText))
thinkTagOpened = true
thinkTagClosed = false
}
thinkingContent.WriteString(response.Thinking)
displayResponse(response.Thinking, opts.WordWrap, state)
displayResponseTo(thinkWriter, thinkFd, response.Thinking, opts.WordWrap, state)
}
if thinkTagOpened && !thinkTagClosed && (content != "" || len(response.ToolCalls) > 0) {
if !strings.HasSuffix(thinkingContent.String(), "\n") {
fmt.Println()
fmt.Fprintln(thinkWriter)
}
fmt.Print(thinkingOutputClosingText(plainText))
fmt.Fprint(thinkWriter, thinkingOutputClosingText(thinkPlainText))
thinkTagOpened = false
thinkTagClosed = true
state = &displayResponseState{}
@ -2155,6 +2187,7 @@ func NewCLI() *cobra.Command {
runCmd.Flags().String("think", "", "Enable thinking mode: true/false or high/medium/low for supported models")
runCmd.Flags().Lookup("think").NoOptDefVal = "true"
runCmd.Flags().Bool("hidethinking", false, "Hide thinking output (if provided)")
runCmd.Flags().Bool("thinkingstderr", false, "Output thinking to stderr instead of stdout")
runCmd.Flags().Bool("truncate", false, "For embedding models: truncate inputs exceeding context length (default: true). Set --truncate=false to error instead")
runCmd.Flags().Int("dimensions", 0, "Truncate output embeddings to specified dimension (embedding models only)")
runCmd.Flags().Bool("experimental", false, "Enable experimental agent loop with tools")

View file

@ -433,6 +433,7 @@ func TestRunEmbeddingModel(t *testing.T) {
cmd.Flags().String("format", "", "")
cmd.Flags().String("think", "", "")
cmd.Flags().Bool("hidethinking", false, "")
cmd.Flags().Bool("thinkingstderr", false, "")
oldStdout := os.Stdout
r, w, _ := os.Pipe()
@ -525,6 +526,7 @@ func TestRunEmbeddingModelWithFlags(t *testing.T) {
cmd.Flags().String("format", "", "")
cmd.Flags().String("think", "", "")
cmd.Flags().Bool("hidethinking", false, "")
cmd.Flags().Bool("thinkingstderr", false, "")
if err := cmd.Flags().Set("truncate", "true"); err != nil {
t.Fatalf("failed to set truncate flag: %v", err)
@ -626,6 +628,7 @@ func TestRunEmbeddingModelPipedInput(t *testing.T) {
cmd.Flags().String("format", "", "")
cmd.Flags().String("think", "", "")
cmd.Flags().Bool("hidethinking", false, "")
cmd.Flags().Bool("thinkingstderr", false, "")
// Capture stdin
oldStdin := os.Stdin
@ -701,6 +704,7 @@ func TestRunEmbeddingModelNoInput(t *testing.T) {
cmd.Flags().String("format", "", "")
cmd.Flags().String("think", "", "")
cmd.Flags().Bool("hidethinking", false, "")
cmd.Flags().Bool("thinkingstderr", false, "")
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
@ -752,6 +756,7 @@ func TestRunHandler_CloudAuthErrorOnShow_PrintsSigninMessage(t *testing.T) {
cmd.Flags().String("format", "", "")
cmd.Flags().String("think", "", "")
cmd.Flags().Bool("hidethinking", false, "")
cmd.Flags().Bool("thinkingstderr", false, "")
oldStdout := os.Stdout
readOut, writeOut, _ := os.Pipe()
@ -820,6 +825,7 @@ func TestRunHandler_CloudAuthErrorOnGenerate_PrintsSigninMessage(t *testing.T) {
cmd.Flags().String("format", "", "")
cmd.Flags().String("think", "", "")
cmd.Flags().Bool("hidethinking", false, "")
cmd.Flags().Bool("thinkingstderr", false, "")
oldStdout := os.Stdout
readOut, writeOut, _ := os.Pipe()
@ -904,6 +910,7 @@ func TestRunHandler_ExplicitCloudStubMissing_PullsNormalizedNameTEMP(t *testing.
cmd.Flags().String("format", "", "")
cmd.Flags().String("think", "", "")
cmd.Flags().Bool("hidethinking", false, "")
cmd.Flags().Bool("thinkingstderr", false, "")
err := RunHandler(cmd, []string{"gpt-oss:20b:cloud", "hi"})
if err != nil {
@ -975,6 +982,7 @@ func TestRunHandler_ExplicitCloudStubPresent_SkipsPullTEMP(t *testing.T) {
cmd.Flags().String("format", "", "")
cmd.Flags().String("think", "", "")
cmd.Flags().Bool("hidethinking", false, "")
cmd.Flags().Bool("thinkingstderr", false, "")
err := RunHandler(cmd, []string{"gpt-oss:20b:cloud", "hi"})
if err != nil {
@ -1042,6 +1050,7 @@ func TestRunHandler_ExplicitCloudStubPullFailure_IsBestEffortTEMP(t *testing.T)
cmd.Flags().String("format", "", "")
cmd.Flags().String("think", "", "")
cmd.Flags().Bool("hidethinking", false, "")
cmd.Flags().Bool("thinkingstderr", false, "")
err := RunHandler(cmd, []string{"gpt-oss:20b:cloud", "hi"})
if err != nil {

View file

@ -149,8 +149,9 @@ type RunOptions struct {
Options map[string]any
KeepAlive *api.Duration
Think *api.ThinkValue
HideThinking bool
Verbose bool
HideThinking bool
ThinkingToStderr bool
Verbose bool
// Agent fields (managed externally for session persistence)
Tools *tools.Registry
@ -201,32 +202,42 @@ func Chat(ctx context.Context, opts RunOptions) (*api.Message, error) {
var consecutiveErrors int // Track consecutive 500 errors for retry limit
var latest api.ChatResponse
showThinking := !opts.HideThinking || opts.ThinkingToStderr
thinkWriter := io.Writer(os.Stdout)
thinkFd := int(os.Stdout.Fd())
thinkPlainText := false
if opts.ThinkingToStderr {
thinkWriter = os.Stderr
thinkFd = int(os.Stderr.Fd())
thinkPlainText = !term.IsTerminal(thinkFd)
}
role := "assistant"
messages := opts.Messages
fn := func(response api.ChatResponse) error {
if response.Message.Content != "" || !opts.HideThinking {
if response.Message.Content != "" || showThinking {
p.StopAndClear()
}
latest = response
role = response.Message.Role
if response.Message.Thinking != "" && !opts.HideThinking {
if response.Message.Thinking != "" && showThinking {
if !thinkTagOpened {
fmt.Print(thinkingOutputOpeningText(false))
fmt.Fprint(thinkWriter, thinkingOutputOpeningText(thinkPlainText))
thinkTagOpened = true
thinkTagClosed = false
}
thinkingContent.WriteString(response.Message.Thinking)
displayResponse(response.Message.Thinking, opts.WordWrap, state)
displayResponseTo(thinkWriter, thinkFd, response.Message.Thinking, opts.WordWrap, state)
}
content := response.Message.Content
if thinkTagOpened && !thinkTagClosed && (content != "" || len(response.Message.ToolCalls) > 0) {
if !strings.HasSuffix(thinkingContent.String(), "\n") {
fmt.Println()
fmt.Fprintln(thinkWriter)
}
fmt.Print(thinkingOutputClosingText(false))
fmt.Fprint(thinkWriter, thinkingOutputClosingText(thinkPlainText))
thinkTagOpened = false
thinkTagClosed = true
state = &displayResponseState{}
@ -549,12 +560,16 @@ type displayResponseState struct {
}
func displayResponse(content string, wordWrap bool, state *displayResponseState) {
termWidth, _, _ := term.GetSize(int(os.Stdout.Fd()))
displayResponseTo(os.Stdout, int(os.Stdout.Fd()), content, wordWrap, state)
}
func displayResponseTo(w io.Writer, fd int, content string, wordWrap bool, state *displayResponseState) {
termWidth, _, _ := term.GetSize(fd)
if wordWrap && termWidth >= 10 {
for _, ch := range content {
if state.lineLength+1 > termWidth-5 {
if len(state.wordBuffer) > termWidth-10 {
fmt.Printf("%s%c", state.wordBuffer, ch)
fmt.Fprintf(w, "%s%c", state.wordBuffer, ch)
state.wordBuffer = ""
state.lineLength = 0
continue
@ -563,14 +578,14 @@ func displayResponse(content string, wordWrap bool, state *displayResponseState)
// backtrack the length of the last word and clear to the end of the line
a := len(state.wordBuffer)
if a > 0 {
fmt.Printf("\x1b[%dD", a)
fmt.Fprintf(w, "\x1b[%dD", a)
}
fmt.Printf("\x1b[K\n")
fmt.Printf("%s%c", state.wordBuffer, ch)
fmt.Fprintf(w, "\x1b[K\n")
fmt.Fprintf(w, "%s%c", state.wordBuffer, ch)
state.lineLength = len(state.wordBuffer) + 1
} else {
fmt.Print(string(ch))
fmt.Fprint(w, string(ch))
state.lineLength++
switch ch {
@ -585,7 +600,7 @@ func displayResponse(content string, wordWrap bool, state *displayResponseState)
}
}
} else {
fmt.Printf("%s%s", state.wordBuffer, content)
fmt.Fprintf(w, "%s%s", state.wordBuffer, content)
if len(state.wordBuffer) > 0 {
state.wordBuffer = ""
}
@ -662,7 +677,7 @@ func checkModelCapabilities(ctx context.Context, modelName string) (supportsTool
// This is called from cmd.go when --experimental flag is set.
// If yoloMode is true, all tool approvals are skipped.
// If enableWebsearch is true, the web search tool is registered.
func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, options map[string]any, think *api.ThinkValue, hideThinking bool, keepAlive *api.Duration, yoloMode bool, enableWebsearch bool) error {
func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, options map[string]any, think *api.ThinkValue, hideThinking bool, thinkingToStderr bool, keepAlive *api.Duration, yoloMode bool, enableWebsearch bool) error {
scanner, err := readline.New(readline.Prompt{
Prompt: ">>> ",
AltPrompt: "... ",
@ -1058,15 +1073,16 @@ func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, op
verbose, _ := cmd.Flags().GetBool("verbose")
opts := RunOptions{
Model: modelName,
Messages: messages,
WordWrap: wordWrap,
Format: format,
Options: options,
Think: think,
HideThinking: hideThinking,
KeepAlive: keepAlive,
Tools: toolRegistry,
Model: modelName,
Messages: messages,
WordWrap: wordWrap,
Format: format,
Options: options,
Think: think,
HideThinking: hideThinking,
ThinkingToStderr: thinkingToStderr,
KeepAlive: keepAlive,
Tools: toolRegistry,
Approval: approval,
YoloMode: yoloMode,
Verbose: verbose,