Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
348ed80
docs(spec): GLM/DeepSeek Codex support design
Jun 17, 2026
8bd2a72
docs(plan): GLM/DeepSeek Codex support implementation plan
Jun 17, 2026
27689c3
feat(protoconv): add model routing catalog
Jun 17, 2026
3cfd0d2
feat(protoconv): Responses->Chat Completions request mapper
Jun 17, 2026
dd83b7f
feat(protoconv): Chat Completions->Responses response assembler
Jun 17, 2026
e33162c
feat(protoconv): Chat Completions SSE -> Responses SSE
Jun 17, 2026
d6f6774
feat(protoconv): Responses->Anthropic Messages request mapper
Jun 17, 2026
7a8646f
feat(protoconv): Anthropic Messages->Responses response assembler
Jun 17, 2026
283b306
feat(protoconv): Anthropic Messages SSE -> Responses SSE
Jun 17, 2026
88a37ab
fix(protoconv): balance streaming output_item added/done, carry item_…
Jun 17, 2026
2ccd275
feat(modelproxy): route converted models through protoconv
Jun 17, 2026
0b6d877
feat(codex): add SetModel to rewrite only the model field
Jun 17, 2026
bf4229c
feat(agentctl): add set-model subcommand
Jun 17, 2026
b72c4ed
feat(agentserver): add set-model subcommand
Jun 17, 2026
3fa2d93
fix(codex): SetModel preserves existing per-user proxy token
Jun 17, 2026
63b94f1
fix(protoconv,modelproxy): always emit response.completed, drop think…
Jun 17, 2026
1d8d572
fix(protoconv): GLM model name is glm-5.2 on the gateway ([1m] reject…
Jun 18, 2026
b87fdc3
fix(protoconv): Anthropic converter must set max_tokens (gateway requ…
Jun 18, 2026
1ba151f
test(protoconv): regression test against real captured GLM SSE stream
Jun 22, 2026
1226391
fix(protoconv): handle bare-string input in Anthropic request mapper
Jun 22, 2026
59619ff
build(packaging): skip Authenticode check on hosts without PowerShell
Jun 22, 2026
064a5e7
fix(codex): launcher's provider-only update must preserve user-select…
Jun 22, 2026
3452c5c
fix(protoconv): carry upstream id+model into streaming response.creat…
Jun 22, 2026
fdcde5a
fix(protoconv): emit ResponseItem-shaped output_item events for Codex
Jun 22, 2026
13f673f
feat(console-ui): model selector in 星池指挥官 dashboard
Jun 22, 2026
22060be
fix(protoconv): merge developer role into system for Chat Completions
Jun 22, 2026
e95ed89
fix(protoconv): drain Chat SSE body after [DONE] so upstream sees cle…
Jun 22, 2026
c83ded4
fix(launcher): tolerate locked config.toml when launching Codex Desktop
Jun 23, 2026
c20df28
feat(codexdesktop): expose GLM/DeepSeek in Codex Desktop's own model …
Jun 23, 2026
1187521
Revert "feat(codexdesktop): expose GLM/DeepSeek in Codex Desktop's ow…
Jun 23, 2026
d60aa06
fix(pr12-review): address P1/P2/P3/P4 review findings
Jun 23, 2026
78d42d4
fix(protoconv): drop reasoning forward in Chat converter (PR #12 revi…
Jun 23, 2026
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
56 changes: 56 additions & 0 deletions cmd/agentctl/cmd_set_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"fmt"

"github.com/agentserver/agentserver-pkg/internal/codex"
"github.com/agentserver/agentserver-pkg/internal/paths"
"github.com/agentserver/agentserver-pkg/internal/protoconv"
)

func runSetModel(args []string) {
if err := runSetModelWithConfigResolved(args); err != nil {
die(err)
}
}

// runSetModelWithConfigResolved resolves the real Codex config path and applies.
func runSetModelWithConfigResolved(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: agentctl set-model <name>; known models: %v", protoconv.KnownModels())
}
if err := validateModelSelection(args[0]); err != nil {
return err
}
p, err := paths.Default()
if err != nil {
return err
}
return runSetModelWithConfig(p.CodexConfigFile, args)
}

// runSetModelWithConfig applies the selection to an explicit config path (testable).
func runSetModelWithConfig(configPath string, args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: set-model <name>; known models: %v", protoconv.KnownModels())
}
if err := validateModelSelection(args[0]); err != nil {
return err
}
if err := codex.SetModel(configPath, args[0]); err != nil {
return err
}
fmt.Printf("model set to %s in %s\n", args[0], configPath)
return nil
}

// validateModelSelection rejects models not in the protoconv catalog.
func validateModelSelection(model string) error {
if model == "" {
return fmt.Errorf("model name required; known models: %v", protoconv.KnownModels())
}
if _, ok := protoconv.LookupRoute(model); !ok {
return fmt.Errorf("unknown model %q; known models: %v", model, protoconv.KnownModels())
}
return nil
}
46 changes: 46 additions & 0 deletions cmd/agentctl/cmd_set_model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"os"
"path/filepath"
"testing"
)

func TestValidateModelSelection(t *testing.T) {
cases := []struct {
model string
ok bool
}{
{"gpt-5.5", true},
{"deepseek-v4-pro", true},
{"glm-5.2", true},
{"bogus-model", false},
{"", false},
}
for _, c := range cases {
if err := validateModelSelection(c.model); (err == nil) != c.ok {
t.Errorf("validateModelSelection(%q) err=%v, want ok=%v", c.model, err, c.ok)
}
}
}

func TestRunSetModelWritesConfig(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.toml")
if err := runSetModelWithConfig(path, []string{"deepseek-v4-pro"}); err != nil {
t.Fatalf("err: %v", err)
}
b, _ := os.ReadFile(path)
if !contains(string(b), `model = "deepseek-v4-pro"`) {
t.Errorf("model not written; got:\n%s", b)
}
}

func contains(s, sub string) bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
3 changes: 3 additions & 0 deletions cmd/agentctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ func main() {
if err := runInstallCodex(os.Args[2:]); err != nil {
die(err)
}
case "set-model":
runSetModel(os.Args[2:])
case "test-install-vscode":
runTestInstallVSCode()
case "test-install-codex-desktop":
Expand Down Expand Up @@ -54,6 +56,7 @@ USAGE:
agentctl logs print last 200 lines of launcher log
agentctl install-codex --manifest <path>
download and install Codex runtime from npm mirrors
agentctl set-model <name> set the Codex model (gpt-5.5 / deepseek-v4-pro / glm-5.2)

P13.4 verification subcommands (skip the OAuth steps, exercise everything else):
agentctl test-install-vscode download + run the VS Code Microsoft Store bootstrapper
Expand Down
31 changes: 31 additions & 0 deletions cmd/agentserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import (
"strings"
"syscall"

"github.com/agentserver/agentserver-pkg/internal/codex"
"github.com/agentserver/agentserver-pkg/internal/headless"
"github.com/agentserver/agentserver-pkg/internal/modelaccess"
"github.com/agentserver/agentserver-pkg/internal/modelserver"
"github.com/agentserver/agentserver-pkg/internal/oauth"
"github.com/agentserver/agentserver-pkg/internal/paths"
"github.com/agentserver/agentserver-pkg/internal/protoconv"
"github.com/agentserver/agentserver-pkg/internal/secrets"
"github.com/agentserver/agentserver-pkg/internal/terminalauth"
"golang.org/x/term"
Expand All @@ -32,6 +34,7 @@ type app struct {
switchWorkspace func(context.Context) error
serveDriverMCP func(context.Context) error
runDaemon func(context.Context) error
setModel func(args []string) error
}

func main() {
Expand Down Expand Up @@ -165,6 +168,9 @@ func newApp() app {
Logf: daemonLogf(proxyLogPath),
})
},
setModel: func(args []string) error {
return runAgentserverSetModel(args)
},
}
}

Expand All @@ -191,6 +197,12 @@ func (a app) run(ctx context.Context, args []string) error {
if cmd == "model-proxy-daemon" {
return a.runDaemon(ctx)
}
if cmd == "set-model" {
if a.setModel == nil {
return fmt.Errorf("set-model unavailable: paths not initialized")
}
return a.setModel(args[1:])
}
switch cmd {
case "", "install-driver", "switch-workspace", "serve-driver-mcp":
default:
Expand Down Expand Up @@ -230,6 +242,25 @@ func promptName(defaultName string) (string, error) {
})
}

func runAgentserverSetModel(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: agentserver set-model <name>; known models: %v", protoconv.KnownModels())
}
model := args[0]
if _, ok := protoconv.LookupRoute(model); !ok {
return fmt.Errorf("unknown model %q; known models: %v", model, protoconv.KnownModels())
}
p, err := paths.Default()
if err != nil {
return err
}
if err := codex.SetModel(p.CodexConfigFile, model); err != nil {
return err
}
fmt.Printf("model set to %s in %s\n", model, p.CodexConfigFile)
return nil
}

func promptNameWithTerminal(r io.Reader, w io.Writer, defaultName string, isTerminal func() bool) (string, error) {
if isTerminal == nil || !isTerminal() {
return defaultName, nil
Expand Down
20 changes: 17 additions & 3 deletions cmd/launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ func serveCompletedConsole(ctx context.Context, in completedServeInput) error {
Slaves: slaveManager,
Updates: updates,
PendingSlaveRestartsPath: in.Paths.PendingSlaveRestartsFile,
CodexConfigFile: in.Paths.CodexConfigFile,
ModelserverWebBaseURL: "https://code.cs.ac.cn",
RefreshModelserverToken: func(ctx context.Context) error {
_, err := tokenrefresh.RefreshOnce(ctx, tokenrefresh.Options{
Expand Down Expand Up @@ -806,8 +807,9 @@ func launchCompletedOpenCodeDesktop(ctx context.Context, s *state.State, p paths
if err != nil {
return err
}
// See launchCompletedCodexDesktop for why this is tolerant of failure.
if err := codex.UpdateConfig(p.CodexConfigFile, codex.ModelserverProxySettings(modelproxy.DefaultBaseURL, localProxyToken)); err != nil {
return err
log.Printf("launcher: update codex config (continuing): %v", err)
}
if p.OpenCodeConfigFile != "" {
if err := opencode.UpdateConfig(p.OpenCodeConfigFile, opencode.Settings{
Expand Down Expand Up @@ -858,8 +860,15 @@ func launchCompletedCodexDesktop(ctx context.Context, s *state.State, p paths.Pa
if err != nil {
return err
}
// UpdateConfig writes config.toml atomically (write tmp, rename). If Codex
// Desktop is already running it holds a read handle and Windows rejects
// the rename with ERROR_ACCESS_DENIED. That's a benign no-op for "open
// frontend" — the running Codex won't reload the file anyway — so log and
// continue rather than fail the launch. First-time startup (Codex not
// running, file unlocked) still writes normally; codex.SetModel (user
// switching models) is invoked when Codex isn't holding the file open.
if err := codex.UpdateConfig(p.CodexConfigFile, codex.ModelserverProxySettings(modelproxy.DefaultBaseURL, localProxyToken)); err != nil {
return err
log.Printf("launcher: update codex config (continuing): %v", err)
}
_ = env.PersistUserEnv(codex.LocalProxyAPIKeyEnv, localProxyToken)
_ = os.Setenv(codex.LocalProxyAPIKeyEnv, localProxyToken)
Expand Down Expand Up @@ -1028,8 +1037,13 @@ func execVSCode(codeExe string, p paths.Paths, folder string, sec secrets.Store,
if err != nil {
return err
}
// Unlike Codex/OpenCode Desktop, VS Code does NOT hold the codex config
// open, so the rename-while-locked race that justifies tolerating
// UpdateConfig failures in those paths doesn't apply here. We need a
// fresh codex config before VS Code starts (the Codex extension reads it
// at session creation), so propagate the error.
if err := codex.UpdateConfig(p.CodexConfigFile, codex.ModelserverProxySettings(modelproxy.DefaultBaseURL, localProxyToken)); err != nil {
return err
return fmt.Errorf("update codex config before launching VS Code: %w", err)
}
_ = env.PersistUserEnv(codex.LocalProxyAPIKeyEnv, localProxyToken)
_ = os.Setenv(codex.LocalProxyAPIKeyEnv, localProxyToken)
Expand Down
Loading
Loading