Files
magpie/internal/provider/opencode_client.go
T

108 lines
4.1 KiB
Go

package provider
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"regexp"
"strings"
)
// OpenCodeVersion is the OpenCode release magpie says it is to OpenCode's
// gateway. Zen's free tier turns away anything older than 1.18.0 (426
// "OpenCode 1.18.0 or newer is required to use the free tier").
const OpenCodeVersion = "1.18.34"
// OpenCodeAnonymousKey is the key OpenCode asks Zen with when no one is
// signed in to it, which Zen's free models answer.
const OpenCodeAnonymousKey = "public"
// OpenCodeClient makes a request to OpenCode's gateway (Zen or Go) carry
// what OpenCode itself sends there (packages/opencode/src/session/llm/
// request.ts, for a provider whose id starts with "opencode"): its
// User-Agent, the session in x-opencode-session, the turn in
// x-opencode-request, and x-opencode-client and x-opencode-project. Zen's
// free models are served to no one else ("OpenCode's free tier can only be
// used from within OpenCode"). session names the conversation; one already
// in OpenCode's form (ses_…) goes as it is, any other becomes one, the same
// for the same conversation, which Zen routes and caches by.
func OpenCodeClient(h http.Header, session string) {
h.Set("User-Agent", "opencode/"+OpenCodeVersion)
h.Set("x-opencode-session", openCodeID("ses", session))
h.Set("x-opencode-request", openCodeID("msg", ""))
h.Set("x-opencode-client", "cli")
// OpenCode's project outside a git repository
h.Set("x-opencode-project", "global")
}
// OpenCodeFree says whether model is one of OpenCode Zen's free ones on p,
// which Zen serves only to what looks like OpenCode: besides the headers
// OpenCodeClient sets, a streamed request offering tools named bash and
// read (lowercase, by name alone; checked against Zen, October 2026). The
// gateway asks them so whoever the client is (gateway/zenfree.go), and the
// Test button's probe so (zenFreeProbe).
func (p Provider) OpenCodeFree(model string) bool {
return p.IsOpenCode() && strings.HasSuffix(model, "-free")
}
// OpenCodeTools are the tools Zen's free tier wants offered, by name.
var OpenCodeTools = []string{"bash", "read"}
// OpenCodeStub describes a tool offered only for Zen's free tier to see,
// which the client doesn't have.
const OpenCodeStub = "Not available in this session. Never call this tool."
// zenFreeProbe is the Test button's smallest request, body, asked as Zen's
// free tier wants it: streamed, offering bash and read, which say they
// aren't to be called.
func zenFreeProbe(proto Protocol, body string) string {
var m map[string]any
if json.Unmarshal([]byte(body), &m) != nil {
return body
}
var tools []any
for _, n := range OpenCodeTools {
schema := map[string]any{"type": "object", "properties": map[string]any{}}
switch proto {
case Chat:
tools = append(tools, map[string]any{"type": "function", "function": map[string]any{"name": n, "description": OpenCodeStub, "parameters": schema}})
case Responses:
tools = append(tools, map[string]any{"type": "function", "name": n, "description": OpenCodeStub, "parameters": schema})
default:
tools = append(tools, map[string]any{"name": n, "description": OpenCodeStub, "input_schema": schema})
}
}
m["stream"], m["tools"] = true, tools
b, err := json.Marshal(m)
if err != nil {
return body
}
return string(b)
}
var openCodeIDRe = regexp.MustCompile(`^[a-z]{3}_[0-9a-f]{12}[0-9A-Za-z]{14}$`)
const base62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
// openCodeID is an id as OpenCode makes them (prefix_, 12 hex digits and 14
// base62 ones): seed's own when it is one with this prefix, else made from
// seed, or at random when there is none.
func openCodeID(prefix, seed string) string {
if strings.HasPrefix(seed, prefix+"_") && openCodeIDRe.MatchString(seed) {
return seed
}
var b [32]byte
if seed != "" {
b = sha256.Sum256([]byte(seed))
} else if _, err := rand.Read(b[:]); err != nil {
b = sha256.Sum256([]byte(randomUUID()))
}
id := []byte(prefix + "_" + hex.EncodeToString(b[:6]))
for _, c := range b[6:20] {
id = append(id, base62[int(c)%len(base62)])
}
return string(id)
}