From 7fdcf01534f2ffdc61c8156ae117edf5e2c4e948 Mon Sep 17 00:00:00 2001 From: Rene Date: Mon, 25 May 2026 17:22:32 +0800 Subject: [PATCH] archon compat --- cmd/ocgo/main.go | 63 ++++++++++++++++++++++++++++++++----------- cmd/ocgo/main_test.go | 14 +++++----- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/cmd/ocgo/main.go b/cmd/ocgo/main.go index 8aabb40..fd27350 100644 --- a/cmd/ocgo/main.go +++ b/cmd/ocgo/main.go @@ -35,6 +35,7 @@ type Config struct { APIKey string `json:"api_key"` Host string `json:"host"` Port int `json:"port"` + Model string `json:"model"` } type AnthropicRequest struct { @@ -202,6 +203,13 @@ func modelContextWindow(model string) int { } } +func modelDisplayName(model string) string { + if modelContextWindow(model) >= 1000000 { + return model + "[1M]" + } + return model +} + func modelSupportsImages(model string) bool { switch model { case "kimi-k2.6", "kimi-k2.5", "mimo-v2-omni": @@ -299,17 +307,22 @@ func launchCmd() *cobra.Command { func serveCmd() *cobra.Command { var background bool + var model string cmd := &cobra.Command{Use: "serve", Short: "Start local Anthropic-compatible proxy", RunE: func(cmd *cobra.Command, args []string) error { if background { - return startBackground() + return startBackground(model) } cfg, err := loadConfig() if err != nil { return err } + if model != "" { + cfg.Model = model + } return runServer(cfg) }} cmd.Flags().BoolVarP(&background, "background", "b", false, "Run proxy in the background") + cmd.Flags().StringVarP(&model, "model", "m", "", "Upstream model ID (default: kimi-k2.6)") return cmd } @@ -384,7 +397,7 @@ func proxyMessages(w http.ResponseWriter, r *http.Request, cfg Config) { http.Error(w, "invalid request", http.StatusBadRequest) return } - or := convertRequest(ar) + or := convertRequest(ar, cfg.Model) if err := validateImageSupport(or); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -409,10 +422,10 @@ func proxyMessages(w http.ResponseWriter, r *http.Request, cfg Config) { return } if ar.Stream { - streamAnthropic(w, resp.Body, or.Model) + streamAnthropic(w, resp.Body, modelDisplayName(or.Model)) return } - writeAnthropicResponse(w, resp.Body, or.Model) + writeAnthropicResponse(w, resp.Body, modelDisplayName(or.Model)) } func proxyChatCompletions(w http.ResponseWriter, r *http.Request, cfg Config) { @@ -430,6 +443,17 @@ func proxyChatCompletions(w http.ResponseWriter, r *http.Request, cfg Config) { http.Error(w, err.Error(), http.StatusBadRequest) return } + // Apply default model if configured + if cfg.Model != "" { + dm := cleanModelName(cfg.Model) + var reqMap map[string]any + if json.Unmarshal(body, &reqMap) == nil { + if m, _ := reqMap["model"].(string); m == "" || strings.HasPrefix(m, "claude-") { + reqMap["model"] = dm + body, _ = json.Marshal(reqMap) + } + } + } req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, openAIURL, bytes.NewReader(body)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -458,7 +482,7 @@ func proxyResponses(w http.ResponseWriter, r *http.Request, cfg Config) { http.Error(w, "invalid request", http.StatusBadRequest) return } - or := responsesToChat(rr) + or := responsesToChat(rr, cfg.Model) if err := validateImageSupport(or); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -483,10 +507,10 @@ func proxyResponses(w http.ResponseWriter, r *http.Request, cfg Config) { return } if rr.Stream { - streamResponses(w, resp.Body, or.Model) + streamResponses(w, resp.Body, modelDisplayName(or.Model)) return } - writeResponsesResponse(w, resp.Body, or.Model) + writeResponsesResponse(w, resp.Body, modelDisplayName(or.Model)) } func copyHeaders(dst, src http.Header) { @@ -601,9 +625,12 @@ func stripRawChatImageDetails(req map[string]any) bool { return changed } -func convertRequest(ar AnthropicRequest) OAIRequest { +func convertRequest(ar AnthropicRequest, defaultModel string) OAIRequest { model := cleanModelName(ar.Model) - if model == "" || strings.HasPrefix(model, "claude-") { + if model == "" || strings.HasPrefix(model, "claude-") || defaultModel != "" { + model = cleanModelName(defaultModel) + } + if model == "" { model = "kimi-k2.6" } out := OAIRequest{Model: model, Stream: ar.Stream, StreamOptions: streamUsageOptions(ar.Stream), MaxTokens: ar.MaxTokens, Temperature: ar.Temperature, TopP: ar.TopP} @@ -619,8 +646,11 @@ func convertRequest(ar AnthropicRequest) OAIRequest { return out } -func responsesToChat(rr ResponsesRequest) OAIRequest { +func responsesToChat(rr ResponsesRequest, defaultModel string) OAIRequest { model := cleanModelName(rr.Model) + if model == "" || defaultModel != "" { + model = cleanModelName(defaultModel) + } if model == "" { model = "kimi-k2.6" } @@ -1392,7 +1422,7 @@ func ensureServer(base string) error { if healthy(base) { return nil } - if err := startBackground(); err != nil { + if err := startBackground(""); err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -1410,7 +1440,7 @@ func startLaunchServer(base string) (*exec.Cmd, error) { if healthy(base) { return nil, nil } - cmd, err := startServerProcess(false) + cmd, err := startServerProcess(false, "") if err != nil { return nil, err } @@ -1445,12 +1475,12 @@ func healthy(base string) bool { return resp.StatusCode == 200 } -func startBackground() error { - _, err := startServerProcess(true) +func startBackground(model string) error { + _, err := startServerProcess(true, model) return err } -func startServerProcess(detached bool) (*exec.Cmd, error) { +func startServerProcess(detached bool, model string) (*exec.Cmd, error) { bin, err := os.Executable() if err != nil { return nil, err @@ -1459,6 +1489,9 @@ func startServerProcess(detached bool) (*exec.Cmd, error) { return nil, err } args := []string{"serve"} + if model != "" { + args = append(args, "--model", model) + } cmd := exec.Command(bin, args...) logf, err := os.OpenFile(filepath.Join(configDir(), "ocgo.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { diff --git a/cmd/ocgo/main_test.go b/cmd/ocgo/main_test.go index f802032..11a1397 100644 --- a/cmd/ocgo/main_test.go +++ b/cmd/ocgo/main_test.go @@ -217,7 +217,7 @@ func TestResponsesInputPreservesImages(t *testing.T) { func TestResponsesImageKeepsKimiModel(t *testing.T) { req := ResponsesRequest{Model: "kimi-k2.6", Input: []byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":"describe this"},{"type":"input_image","image_url":"data:image/png;base64,abc"}]}]`)} - out := responsesToChat(req) + out := responsesToChat(req, "") if out.Model != "kimi-k2.6" { t.Fatalf("image request should keep Kimi model, got %q", out.Model) } @@ -228,7 +228,7 @@ func TestResponsesImageKeepsKimiModel(t *testing.T) { func TestResponsesImageRejectsUnsupportedModel(t *testing.T) { req := ResponsesRequest{Model: "deepseek-v4-pro", Input: []byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":"describe this"},{"type":"input_image","image_url":"data:image/png;base64,abc"}]}]`)} - out := responsesToChat(req) + out := responsesToChat(req, "") if err := validateImageSupport(out); err == nil || !strings.Contains(err.Error(), "deepseek-v4-pro") { t.Fatalf("DeepSeek image request should be rejected, got %v", err) } @@ -273,15 +273,15 @@ func TestRawChatStreamRequestsUsage(t *testing.T) { } func TestConvertedStreamingRequestsAskForUsage(t *testing.T) { - anthropic := convertRequest(AnthropicRequest{Model: "kimi-k2.6", Stream: true, Messages: []AMessage{{Role: "user", Content: []byte(`hello`)}}}) + anthropic := convertRequest(AnthropicRequest{Model: "kimi-k2.6", Stream: true, Messages: []AMessage{{Role: "user", Content: []byte(`hello`)}}}, "") if anthropic.StreamOptions == nil || !anthropic.StreamOptions.IncludeUsage { t.Fatalf("anthropic conversion should request stream usage: %+v", anthropic.StreamOptions) } - responses := responsesToChat(ResponsesRequest{Model: "kimi-k2.6", Stream: true, Input: []byte(`"hello"`)}) + responses := responsesToChat(ResponsesRequest{Model: "kimi-k2.6", Stream: true, Input: []byte(`"hello"`)}, "") if responses.StreamOptions == nil || !responses.StreamOptions.IncludeUsage { t.Fatalf("responses conversion should request stream usage: %+v", responses.StreamOptions) } - plain := responsesToChat(ResponsesRequest{Model: "kimi-k2.6", Input: []byte(`"hello"`)}) + plain := responsesToChat(ResponsesRequest{Model: "kimi-k2.6", Input: []byte(`"hello"`)}, "") if plain.StreamOptions != nil { t.Fatalf("non-streaming conversion should not set stream options: %+v", plain.StreamOptions) } @@ -305,7 +305,7 @@ func TestAnthropicContentPreservesImages(t *testing.T) { } func TestAnthropicImageKeepsKimiModel(t *testing.T) { - out := convertRequest(AnthropicRequest{Model: "kimi-k2.6", Messages: []AMessage{{Role: "user", Content: []byte(`[{"type":"text","text":"what is this?"},{"type":"image","source":{"type":"base64","media_type":"image/png","data":"abc"}}]`)}}}) + out := convertRequest(AnthropicRequest{Model: "kimi-k2.6", Messages: []AMessage{{Role: "user", Content: []byte(`[{"type":"text","text":"what is this?"},{"type":"image","source":{"type":"base64","media_type":"image/png","data":"abc"}}]`)}}}, "") if out.Model != "kimi-k2.6" { t.Fatalf("image request should keep Kimi model, got %q", out.Model) } @@ -315,7 +315,7 @@ func TestAnthropicImageKeepsKimiModel(t *testing.T) { } func TestAnthropicImageRejectsUnsupportedModel(t *testing.T) { - out := convertRequest(AnthropicRequest{Model: "deepseek-v4-pro", Messages: []AMessage{{Role: "user", Content: []byte(`[{"type":"text","text":"what is this?"},{"type":"image","source":{"type":"base64","media_type":"image/png","data":"abc"}}]`)}}}) + out := convertRequest(AnthropicRequest{Model: "deepseek-v4-pro", Messages: []AMessage{{Role: "user", Content: []byte(`[{"type":"text","text":"what is this?"},{"type":"image","source":{"type":"base64","media_type":"image/png","data":"abc"}}]`)}}}, "") if err := validateImageSupport(out); err == nil || !strings.Contains(err.Error(), "deepseek-v4-pro") { t.Fatalf("DeepSeek image request should be rejected, got %v", err) }