package gateway // PLUGIN-SERVED (see AGENTS.md): Command Code's plan ("commandcode-plan") is // a deprecated built-in subscription served by its plugin, // @magpie-community/opencode-commandcode-auth, once moved onto it // (provider.Moved; the default for a new sign-in). A moved one's sign-ins, // models, requests and usage are all the plugin's, never this code's (only // the move, in migrate*.go, still reads its accounts). A fix here alone // doesn't reach those users; fix the plugin // (github.com/magpie-community/plugins, packages/commandcode) and raise the // mover's min in internal/provider/migrate_side.go. // A Command Code account on the Go plan has no Provider API: its key is // only taken where the CLI itself asks, POST /alpha/generate. The request // is written as the CLI (command-code 1.72.2) writes it — its // createModelClient: // // {config:{workingDir,date,environment,structure,isGitRepo,…},memory:null, // taste:null,skills:null,permissionMode:"standard", // params:{model,messages,tools,system,max_tokens,stream:true, // temperature?,reasoning_effort?}} // // the messages in the AI SDK's parts (toWireMessages): an assistant's // text, tool-call and reasoning; a user's tool results as a message of // role "tool" before its text and images. The reply is a JSON object a // line (readLines, consumeStream): text-delta, reasoning-delta, tool-call // with its input whole, finish with the usage, error. The headers are // buildCommandAuthHeaders'. Every other plan is served on the Provider // API, as before (provider/commandcode_plan.go). import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "os" "runtime" "strings" "sync" "time" "github.com/tidwall/gjson" "github.com/yetone/magpie/internal/provider" ) // cmdGenerate says whether an account is asked at /alpha/generate, where, // and with which key; a var so tests can stand in for it. var cmdGenerate = provider.CommandCodeGenerate // cmdCLIVersion is the CLI the requests say they come from. const cmdCLIVersion = "1.72.2" // cmdMaxTokens is the CLI's max_tokens when none is asked (Hk=64e3). const cmdMaxTokens = 64000 // cmdSession is the x-session-id magpie's requests carry: one for as long // as magpie runs, as one CLI session has one. var cmdSession = sync.OnceValue(newUUID) // cmdGoing says whether p is a Go account, asked at /alpha/generate. func cmdGoing(ctx context.Context, p provider.Provider) bool { _, _, ok := cmdGenerate(p.Via(ctx), p) return ok } // serveCommandCode answers a request through /alpha/generate, for a Go // account. func (s *Server) serveCommandCode(w http.ResponseWriter, r *http.Request, from provider.Protocol, p provider.Provider, api, key, model string, body []byte, usage *Usage) (int, string) { req, err := parse(from, body) if err != nil { return writeError(w, from, 400, err.Error()), err.Error() } req.Model = model if req.Effort != "" && !req.ThinkOff { req.Effort = fitEffort(req.Effort, p.Efforts(model)) } ask := s.askCommandCode(api, key, model) if req.WebSearch && !searching(r.Context()) { if canSearch() { return s.searchReply(w, r, from, "Command Code", req, usage, ask) } } ctx, cancel := context.WithCancel(r.Context()) defer cancel() events, status, msg := ask(ctx, req) if events == nil { return writeError(w, from, status, msg), msg } // its failures keep their statuses (out of credits is another // account's turn), and a reply cut short is an error return relayStatus(w, from, "Command Code", req, events, usage, cancel) } // ---- the request ---------------------------------------------------------- type cmdPart struct { Type string `json:"type"` Text *string `json:"text,omitempty"` ToolCallID string `json:"toolCallId,omitempty"` ToolName string `json:"toolName,omitempty"` Input json.RawMessage `json:"input,omitempty"` Output *cmdOutput `json:"output,omitempty"` Image string `json:"image,omitempty"` MimeType string `json:"mimeType,omitempty"` } type cmdOutput struct { Type string `json:"type"` Value string `json:"value"` } type cmdMessage struct { Role string `json:"role"` Content []cmdPart `json:"content"` } type cmdTool struct { Name string `json:"name"` Description string `json:"description"` InputSchema json.RawMessage `json:"input_schema"` } func cmdText(kind, s string) cmdPart { return cmdPart{Type: kind, Text: &s} } // cmdImage is an image as the CLI sends one: a data URL, and its type. func cmdImage(p Part) (cmdPart, bool) { mt := p.MediaType if mt == "" { mt = "image/png" } switch { case p.Data != "": return cmdPart{Type: "image", Image: "data:" + mt + ";base64," + p.Data, MimeType: mt}, true case p.URL != "": return cmdPart{Type: "image", Image: p.URL, MimeType: p.MediaType}, true } return cmdPart{}, false } // cmdMessages is the conversation in the CLI's wire parts. func cmdMessages(req *Request) []cmdMessage { out := []cmdMessage{} names := map[string]string{} // a tool call's id → its tool for _, m := range req.Messages { if m.Role == "assistant" { var parts []cmdPart for _, p := range m.Parts { switch p.Kind { case Text: if p.Text != "" { parts = append(parts, cmdText("text", p.Text)) } case ToolCall: names[p.ID] = p.Name parts = append(parts, cmdPart{Type: "tool-call", ToolCallID: p.ID, ToolName: p.Name, Input: argsOf(p)}) case Thinking: if p.Text != "" { parts = append(parts, cmdText("reasoning", p.Text)) } } } if len(parts) > 0 { out = append(out, cmdMessage{"assistant", parts}) } continue } var results, said []cmdPart for _, p := range m.Parts { switch p.Kind { case ToolResult: name := names[p.CallID] if name == "" { name = "unknown" } results = append(results, cmdPart{Type: "tool-result", ToolCallID: p.CallID, ToolName: name, Output: &cmdOutput{Type: "text", Value: p.Text}}) // the CLI's tool results are text alone; the images a tool // returned go with the user's turn after them for _, im := range p.Images { if c, ok := cmdImage(im); ok { said = append(said, c) } } case Text: if p.Text != "" { said = append(said, cmdText("text", p.Text)) } case Image: if c, ok := cmdImage(p); ok { said = append(said, c) } case File: if p.Text != "" { said = append(said, cmdText("text", p.Text)) } } } if len(results) > 0 { out = append(out, cmdMessage{"tool", results}) } if len(said) > 0 { out = append(out, cmdMessage{"user", said}) } } return out } // cmdPlatform is the machine as Node names it (process.platform). func cmdPlatform() string { if runtime.GOOS == "windows" { return "win32" } return runtime.GOOS } // cmdRequest is req as the CLI asks /alpha/generate. There is no project // behind a request through magpie: the config is the CLI's outside a git // repository (buildServerConfig), in the user's home. func cmdRequest(req *Request, model string) []byte { tools := []cmdTool{} for _, t := range req.Tools { schema := t.Schema if len(schema) == 0 { schema = json.RawMessage(`{"type":"object","properties":{}}`) } tools = append(tools, cmdTool{t.Name, t.Description, schema}) } params := map[string]any{ "model": model, "messages": cmdMessages(req), "tools": tools, "system": req.System, "max_tokens": cmdMaxTokens, "stream": true, } if req.MaxTokens > 0 { // within what the model gives and Command Code takes: more is // refused, the whole request with it params["max_tokens"] = min(req.MaxTokens, provider.CommandCodeOutputOf(model)) } if req.Temp != nil { params["temperature"] = *req.Temp } if req.Effort != "" && !req.ThinkOff { params["reasoning_effort"] = req.Effort } home, _ := os.UserHomeDir() b, _ := json.Marshal(map[string]any{ "config": map[string]any{ "workingDir": home, "date": time.Now().Format("2006-01-02"), "environment": cmdPlatform(), "structure": []string{}, "isGitRepo": false, "currentBranch": "", "mainBranch": "", "gitStatus": "", "recentCommits": []string{}, }, "memory": nil, "taste": nil, "skills": nil, "permissionMode": "standard", "params": params, }) return b } // cmdHeaders are the CLI's own (buildCommandAuthHeaders). func cmdHeaders(h http.Header, key string) { h.Set("Content-Type", "application/json") h.Set("User-Agent", "cli") h.Set("x-command-code-version", cmdCLIVersion) h.Set("x-cli-environment", "production") h.Set("x-project-slug", "magpie") h.Set("x-taste-learning", "false") h.Set("x-session-id", cmdSession()) h.Set("Authorization", "Bearer "+key) } // ---- the reply ------------------------------------------------------------ // askCommandCode is a round for /alpha/generate. func (s *Server) askCommandCode(api, key, model string) round { return func(ctx context.Context, req *Request) (<-chan Event, int, string) { hr, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimSuffix(api, "/")+"/alpha/generate", bytes.NewReader(cmdRequest(req, model))) if err != nil { return nil, 500, "Command Code: " + err.Error() } cmdHeaders(hr.Header, key) res, err := s.client.Do(hr) if err != nil { return nil, 502, "Command Code: " + err.Error() } if res.StatusCode/100 != 2 { b, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20)) res.Body.Close() status, msg := cmdFailure(res.StatusCode, b) return nil, status, msg } events := make(chan Event, 16) go func() { defer close(events) defer res.Body.Close() decodeCommandCode(ctx, res.Body, events, model) }() return events, 0, "" } } // cmdFailure is the status and message for a failure, from Command // Code's {"error":{"type","message"}} (parseEmbeddedErrorJSON) or its // text: a model the plan hasn't is a 403, as another account's plan may // have it; credits run out a 402, a window's limit a 429. func cmdFailure(status int, b []byte) (int, string) { msg := strings.TrimSpace(firstString(gjson.GetBytes(b, "error.message").String(), gjson.GetBytes(b, "message").String(), gjson.GetBytes(b, "error").String(), string(b))) if msg == "" { msg = http.StatusText(status) } if len(msg) > 600 { msg = msg[:600] } low := strings.ToLower(msg) switch { case strings.Contains(msg, "MODEL_NOT_IN_PLAN:"): status = 403 msg = "Model not in plan: " + strings.TrimSpace(strings.Replace(msg, "MODEL_NOT_IN_PLAN:", "", 1)) case strings.Contains(low, "model_not_in_plan"): status = 403 case strings.Contains(low, "premium_credits_exhausted"), strings.Contains(low, "insufficient credits"): status = 402 msg = strings.TrimSpace(strings.Replace(msg, "PREMIUM_CREDITS_EXHAUSTED:", "", 1)) if !quotaWords.MatchString(msg) { msg = "out of credits: " + msg } case strings.Contains(msg, "RATE_LIMITED"), strings.Contains(low, "usage limit"), strings.Contains(low, "window_limit"): if status < 400 || status == 500 { status = 429 } } if status < 400 || status > 599 { status = 502 } return status, "Command Code: " + msg } func firstString(ss ...string) string { for _, s := range ss { if strings.TrimSpace(s) != "" { return s } } return "" } // cmdLine is one line of the reply, the AI SDK's stream parts as the // server passes them on. type cmdLine struct { Type string `json:"type"` Text string `json:"text"` ToolCallID string `json:"toolCallId"` ToolName string `json:"toolName"` Input json.RawMessage `json:"input"` Args json.RawMessage `json:"args"` ProviderExecuted bool `json:"providerExecuted"` FinishReason string `json:"finishReason"` RawFinishReason string `json:"rawFinishReason"` TotalUsage *struct { Input int `json:"inputTokens"` Output int `json:"outputTokens"` Details *struct { NoCache *int `json:"noCacheTokens"` CacheRead int `json:"cacheReadTokens"` CacheWrite int `json:"cacheWriteTokens"` } `json:"inputTokenDetails"` OutDetails *struct { Reasoning int `json:"reasoningTokens"` } `json:"outputTokenDetails"` } `json:"totalUsage"` CacheWriteTokens int `json:"cacheWriteTokens"` Error json.RawMessage `json:"error"` } // cmdInput is a tool call's input: an object, or the object as a string. func cmdInput(l cmdLine) json.RawMessage { raw := l.Input if len(raw) == 0 || string(raw) == "null" { raw = l.Args } var s string if json.Unmarshal(raw, &s) == nil { return parseArgs(s) } if len(raw) == 0 || string(raw) == "null" { return json.RawMessage("{}") } return parseArgs(string(raw)) } // cmdStreamError is an error line's message and status: a string, or // {message, statusCode}, whose message may hold the HTTP error it was. func cmdStreamError(raw json.RawMessage) (int, string) { e := gjson.ParseBytes(raw) msg, status := e.String(), 0 if e.IsObject() { msg, status = e.Get("message").String(), int(e.Get("statusCode").Int()) } if msg == "" { msg = "Stream error" } // "429 {"error":{…}}", as parseEmbeddedErrorJSON reads it if i := strings.Index(msg, "{"); i >= 0 && gjson.Valid(msg[i:]) { var n int if _, err := fmt.Sscanf(strings.TrimSpace(msg[:i]), "%d", &n); err == nil && n >= 400 { status = n } if m := gjson.Get(msg[i:], "error.message").String(); m != "" { msg = m } } if status == 0 { status = 500 } return cmdFailure(status, []byte(msg)) } // decodeCommandCode turns the reply's lines into events. A reply with no // finish line was cut short (the CLI's "stream ended without a finish // event"): an error, not a shorter answer. func decodeCommandCode(ctx context.Context, body io.Reader, out chan<- Event, model string) { send := func(ev Event) bool { select { case out <- ev: return true case <-ctx.Done(): return false } } if !send(Event{Kind: KStart, MsgID: "msg_" + randomToken()[:24], Model: model}) { return } var u Usage stop, tools, finished := "stop", 0, false br := bufio.NewReaderSize(body, 64<<10) for { line, err := br.ReadString('\n') if s := strings.TrimSpace(line); s != "" { var l cmdLine if json.Unmarshal([]byte(s), &l) == nil { switch l.Type { case "text-delta": if l.Text != "" && !send(Event{Kind: KText, Text: l.Text}) { return } case "reasoning-delta": if l.Text != "" && !send(Event{Kind: KThink, Text: l.Text}) { return } case "tool-call": // one run by the server has its result beside it: the // client has nothing to run if l.ProviderExecuted { break } tools++ id := l.ToolCallID if id == "" { id = "toolu_" + randomToken()[:24] } if !send(Event{Kind: KToolStart, ID: id, Name: l.ToolName}) || !send(Event{Kind: KToolArgs, Text: string(cmdInput(l))}) { return } case "cache-write-tokens": if l.CacheWriteTokens > u.CacheWrite { u.CacheWrite = l.CacheWriteTokens } case "finish": finished = true if t := l.TotalUsage; t != nil { u.Output = t.Output input := t.Input if d := t.Details; d != nil { u.CacheRead = d.CacheRead u.CacheWrite = max(u.CacheWrite, d.CacheWrite) // the AI SDK's inputTokens counts the cache in // it; magpie's Input, as Anthropic's, doesn't if d.NoCache != nil { input = *d.NoCache } else { input = max(0, input-d.CacheRead-d.CacheWrite) } } u.Input = input if t.OutDetails != nil { u.Reasoning = t.OutDetails.Reasoning } } switch l.FinishReason { case "tool-calls": stop = "tool" case "length": stop = "length" case "content-filter": stop = "filter" } case "abort": finished = true case "error": status, msg := cmdStreamError(l.Error) send(Event{Kind: KError, Status: status, Text: msg}) return } } } if err != nil { if !errors.Is(err, io.EOF) { if ctx.Err() == nil { send(Event{Kind: KError, Text: "Command Code: the reply broke off: " + err.Error()}) } return } break } } if !finished { send(Event{Kind: KError, Text: "Command Code: the reply ended before it was complete"}) return } if tools > 0 && stop == "stop" { stop = "tool" } send(Event{Kind: KUsage, Usage: u}) send(Event{Kind: KStop, Stop: stop}) }