mirror of
https://github.com/hanzoai/thinking.git
synced 2026-10-30 14:55:31 +00:00
195 lines
6.5 KiB
Go
195 lines
6.5 KiB
Go
// Copyright 2026 Hanzo AI Inc. All Rights Reserved.
|
|
|
|
// Package thinking folds how-hard-to-think into the native shape each upstream
|
|
// model accepts. Callers speak two dialects — Anthropic sends a token budget,
|
|
// OpenAI sends an effort ordinal — and every upstream reasons by its own control:
|
|
// GLM/DeepSeek by a reasoning_effort ordinal, Qwen by an enable_thinking gate,
|
|
// Kimi by a thinking object. Between them sits ONE neutral value, a Depth ordinal:
|
|
// each dialect is a constructor into it, each upstream a projection out of it.
|
|
//
|
|
// This is the sole home of that map, so ai and zen share one table (as they
|
|
// already share hanzoai/decimal and hanzoai/money). The fold is total and
|
|
// monotone — deeper in, harder out — and lossy where a vocabulary is coarser.
|
|
package thinking
|
|
|
|
// Depth is how hard to think, as an ordinal — the neutral value both wire dialects
|
|
// normalize into and every upstream projects out of. Off means the caller declined
|
|
// to think; the rest ascend.
|
|
type Depth int
|
|
|
|
const (
|
|
Off Depth = iota // don't think — the instant path
|
|
Low // light
|
|
Mid // moderate
|
|
High // hard
|
|
Max // hardest
|
|
)
|
|
|
|
// deep is the token budget at or above which a request is the hardest tier — Claude
|
|
// Code's "ultrathink" is ~32k, "think" ~4k. mid is the moderate threshold.
|
|
const (
|
|
mid = 4096
|
|
deep = 16384
|
|
)
|
|
|
|
// Budget folds an Anthropic thinking.budget_tokens into a Depth. Zero (thinking
|
|
// absent or disabled) is Off; any positive budget is on, at a tier set by size.
|
|
func Budget(tokens int) Depth {
|
|
switch {
|
|
case tokens <= 0:
|
|
return Off
|
|
case tokens < mid:
|
|
return Low
|
|
case tokens < deep:
|
|
return Mid
|
|
default:
|
|
return Max
|
|
}
|
|
}
|
|
|
|
// Effort folds an OpenAI reasoning_effort ordinal into a Depth. An unknown or empty
|
|
// value is Off.
|
|
func Effort(ordinal string) Depth {
|
|
switch ordinal {
|
|
case "low":
|
|
return Low
|
|
case "medium":
|
|
return Mid
|
|
case "high":
|
|
return High
|
|
case "max":
|
|
return Max
|
|
default:
|
|
return Off
|
|
}
|
|
}
|
|
|
|
// On reports whether the caller asked to think at all. Off writes no field, leaving
|
|
// the upstream at its own default.
|
|
func (d Depth) On() bool { return d > Off }
|
|
|
|
// Vocab is the reasoning control an upstream accepts, keyed by request shape — not
|
|
// by brand. Pick it with Of.
|
|
type Vocab string
|
|
|
|
const (
|
|
GLM Vocab = "glm" // reasoning_effort ∈ {none, high, max}: GLM-5.*, DeepSeek V4, the DO-AI default (reasons unless told none)
|
|
OpenAI Vocab = "openai" // reasoning_effort ∈ {low, medium, high}: o-series
|
|
Qwen Vocab = "qwen" // enable_thinking gate (defaults off upstream)
|
|
Zen Vocab = "zen" // enable_thinking gate (defaults on; Off actively disables thinking)
|
|
Kimi Vocab = "kimi" // a thinking object {type, preserve_thinking}
|
|
)
|
|
|
|
// Of returns the vocabulary an upstream model id accepts, keyed by id prefix — the
|
|
// one place brand maps to control shape. Unknown ids fold as GLM: the DO-AI family
|
|
// (glm-5.*, deepseek-*, minimax-*, nemotron-*, llama*) is the universal upstream and
|
|
// its two-tier ordinal is the safe supposition.
|
|
func Of(upstream string) Vocab {
|
|
m := lower(upstream)
|
|
switch {
|
|
case prefix(m, "zen"):
|
|
return Zen
|
|
case prefix(m, "qwen") || prefix(m, "alibaba-qwen"):
|
|
return Qwen
|
|
case prefix(m, "kimi"):
|
|
return Kimi
|
|
case prefix(m, "gpt-") || prefix(m, "openai-gpt") ||
|
|
prefix(m, "o1") || prefix(m, "o3") || prefix(m, "o4"):
|
|
return OpenAI
|
|
default:
|
|
return GLM
|
|
}
|
|
}
|
|
|
|
// Fields projects the depth into the request fields a vocabulary accepts, as a map
|
|
// to merge into an upstream chat body — {} when the vocabulary takes no field for the
|
|
// depth. It is the sole producer of these keys, so a caller clears Keys first: a stale
|
|
// field from another vocabulary must never reach this upstream.
|
|
//
|
|
// Off is not always empty: a GLM-family upstream reasons BY DEFAULT, so Off (the instant
|
|
// path) must actively send reasoning_effort:"none" — the absence of the field would leave
|
|
// the model reasoning, streaming a long content:null preamble that reads as an empty
|
|
// completion. Vocabularies that default OFF (Qwen/Kimi gates, OpenAI o-series) write
|
|
// nothing for Off, as before.
|
|
func (d Depth) Fields(v Vocab) map[string]any {
|
|
switch v {
|
|
case GLM, OpenAI:
|
|
if e := d.ordinal(v); e != "" {
|
|
return map[string]any{"reasoning_effort": e}
|
|
}
|
|
case Zen:
|
|
if d.On() {
|
|
return map[string]any{"enable_thinking": true}
|
|
}
|
|
return map[string]any{"enable_thinking": false}
|
|
case Qwen:
|
|
if d.On() {
|
|
return map[string]any{"enable_thinking": true}
|
|
}
|
|
case Kimi:
|
|
if d.On() {
|
|
think := map[string]any{"type": "enabled"}
|
|
if d == Max {
|
|
think["preserve_thinking"] = "all" // keep the trace across turns for long-horizon coding
|
|
}
|
|
return map[string]any{"thinking": think}
|
|
}
|
|
}
|
|
return map[string]any{}
|
|
}
|
|
|
|
// ordinal folds the depth to a reasoning_effort value valid for an ordinal
|
|
// vocabulary (GLM or OpenAI). GLM reasons by default, so Off maps to "none" (silence the
|
|
// preamble) and it has no low/medium — Low/Mid/High coerce up to "high", Max to "max".
|
|
// OpenAI's o-series reasons only when asked, so Off maps to "" (no field); it has
|
|
// {low,medium,high} and no "max", so Max coerces down to "high".
|
|
func (d Depth) ordinal(v Vocab) string {
|
|
switch v {
|
|
case OpenAI:
|
|
switch d {
|
|
case Low:
|
|
return "low"
|
|
case Mid:
|
|
return "medium"
|
|
case High, Max:
|
|
return "high"
|
|
}
|
|
case GLM:
|
|
switch d {
|
|
case Off:
|
|
// GLM-family upstreams on DO-AI (glm-5.*, deepseek-*, minimax-*, …)
|
|
// REASON BY DEFAULT: sending no reasoning_effort makes them emit a long
|
|
// content:null reasoning stream before any answer token, which a plain
|
|
// OpenAI client renders as an empty completion. Off means "instant path",
|
|
// so it must actively request none — the one value that silences the
|
|
// preamble and streams the answer immediately. (Verified against DO-AI:
|
|
// glm-5.2/deepseek-v4-pro honor "none" and emit zero reasoning chunks.)
|
|
return "none"
|
|
case Low, Mid, High:
|
|
return "high"
|
|
case Max:
|
|
return "max"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Keys is every field any vocabulary may write. A caller clears these before
|
|
// applying Fields so the fold is the sole writer and nothing leaks across a
|
|
// vocabulary boundary.
|
|
var Keys = []string{"reasoning_effort", "enable_thinking", "thinking_budget", "thinking"}
|
|
|
|
// lower and prefix keep the package free of a strings import — a shared leaf earns
|
|
// its keep by staying minimal.
|
|
func lower(s string) string {
|
|
b := []byte(s)
|
|
for i, c := range b {
|
|
if c >= 'A' && c <= 'Z' {
|
|
b[i] = c + ('a' - 'A')
|
|
}
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func prefix(s, p string) bool { return len(s) >= len(p) && s[:len(p)] == p }
|