Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ bin/
.env
.tmp/
oc-go-cc
tmp/
35 changes: 29 additions & 6 deletions cmd/oc-go-cc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"

"github.com/spf13/cobra"
"oc-go-cc/internal/buildinfo"
"oc-go-cc/internal/config"
"oc-go-cc/internal/daemon"
"oc-go-cc/internal/server"
Expand All @@ -19,9 +20,6 @@ const (
pidFileName = "oc-go-cc.pid"
)

// Version is set at build time via -ldflags "-X main.version=...".
var version = "dev"

func main() {
rootCmd := &cobra.Command{
Use: appName,
Expand All @@ -31,7 +29,7 @@ subscription with Claude Code. It intercepts Claude Code's Anthropic API request
transforms them to OpenAI format, and forwards them to OpenCode Go.

Configuration is stored at ~/.config/oc-go-cc/config.json`,
Version: version,
Version: buildinfo.Version,
}

// Add subcommands.
Expand Down Expand Up @@ -89,7 +87,7 @@ func serveCmd() *cobra.Command {
if !daemonize {
if pid, err := daemon.GetPID(pidPath); err == nil {
// Check if process is still running.
if daemon.IsProcessRunning(pid) {
if daemon.IsProcessRunning(pid) && daemon.IsAppProcess(pid, appName) {
return fmt.Errorf("server is already running (PID %d)", pid)
}
// Stale PID file, clean up.
Expand Down Expand Up @@ -144,7 +142,18 @@ func serveCmd() *cobra.Command {
}()
}

fmt.Printf("Starting %s v%s\n", appName, version)
slog.Info("starting proxy",
"binary", buildinfo.BinaryPath(),
"version", buildinfo.Version,
"build_time", buildinfo.BuildTime,
"pid", buildinfo.PID(),
"listen", fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
)

fmt.Printf("Starting %s v%s\n", appName, buildinfo.Version)
fmt.Printf("Binary: %s\n", buildinfo.BinaryPath())
fmt.Printf("Build time: %s\n", buildinfo.BuildTime)
fmt.Printf("PID: %d\n", buildinfo.PID())
fmt.Printf("Listening on %s:%d\n", cfg.Host, cfg.Port)
fmt.Printf("Forwarding to: %s\n", cfg.OpenCodeGo.BaseURL)
fmt.Println()
Expand Down Expand Up @@ -178,6 +187,15 @@ func stopCmd() *cobra.Command {
return fmt.Errorf("server is not running (no PID file)")
}

if !daemon.IsProcessRunning(pid) {
_ = os.Remove(pidPath)
return fmt.Errorf("server is not running (stale PID file)")
}
if !daemon.IsAppProcess(pid, appName) {
_ = os.Remove(pidPath)
return fmt.Errorf("server is not running (PID %d belongs to another process)", pid)
}

if err := daemon.StopProcess(pid); err != nil {
return fmt.Errorf("failed to stop server: %w", err)
}
Expand Down Expand Up @@ -207,6 +225,11 @@ func statusCmd() *cobra.Command {
_ = os.Remove(pidPath)
return nil
}
if !daemon.IsAppProcess(pid, appName) {
fmt.Printf("Server is not running (PID %d belongs to another process)\n", pid)
_ = os.Remove(pidPath)
return nil
}

fmt.Printf("Server is running (PID %d)\n", pid)
return nil
Expand Down
186 changes: 131 additions & 55 deletions configs/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,102 +3,178 @@
"host": "127.0.0.1",
"port": 3456,
"hot_reload": false,
"enable_streaming_scenario_routing": false,
"enable_streaming_scenario_routing": true,
"respect_requested_model": false,

"models": {
"background": {
"provider": "opencode-go",
"model_id": "qwen3.5-plus",
"temperature": 0.5,
"max_tokens": 2048
},
"default": {
"provider": "opencode-go",
"model_id": "kimi-k2.6",
"temperature": 0.7,
"max_tokens": 4096
},
"long_context": {
"provider": "opencode-go",
"model_id": "minimax-m2.5",
"temperature": 0.7,
"max_tokens": 16384,
"context_threshold": 80000
},
"deepseek-v4-pro": {
"provider": "opencode-go",
"model_id": "deepseek-v4-pro",
"temperature": 0.7,
"supports_vision": false,
"temperature": 0.1,
"max_tokens": 8192,
"max_output_tokens": 8192,
"context_window": 1000000,
"context_margin": 8192,
"supports_tools": true,
"reasoning_effort": "max",
"thinking": {
"type": "enabled"
}
"thinking": { "type": "enabled" }
},
"deepseek-v4-flash": {
"fast": {
"provider": "opencode-go",
"model_id": "deepseek-v4-flash",
"temperature": 0.7,
"supports_vision": false,
"temperature": 0.1,
"max_tokens": 4096,
"max_output_tokens": 4096,
"context_window": 1000000,
"context_margin": 8192,
"supports_tools": true,
"reasoning_effort": "max",
"thinking": {
"type": "enabled"
}
"thinking": { "type": "enabled" }
},
"background": {
"provider": "opencode-go",
"model_id": "deepseek-v4-flash",
"supports_vision": false,
"temperature": 0.1,
"max_tokens": 4096,
"max_output_tokens": 4096,
"context_window": 1000000,
"context_margin": 8192,
"supports_tools": true,
"reasoning_effort": "max",
"thinking": { "type": "enabled" }
},
"think": {
"provider": "opencode-go",
"model_id": "glm-5",
"temperature": 0.7,
"max_tokens": 8192
"model_id": "deepseek-v4-pro",
"supports_vision": false,
"temperature": 0.1,
"max_tokens": 8192,
"max_output_tokens": 8192,
"context_window": 1000000,
"context_margin": 8192,
"supports_tools": true,
"reasoning_effort": "max",
"thinking": { "type": "enabled" }
},
"complex": {
"provider": "opencode-go",
"model_id": "glm-5.1",
"temperature": 0.7,
"max_tokens": 4096
"model_id": "deepseek-v4-pro",
"supports_vision": false,
"temperature": 0.1,
"max_tokens": 8192,
"max_output_tokens": 8192,
"context_window": 1000000,
"context_margin": 8192,
"supports_tools": true,
"reasoning_effort": "max",
"thinking": { "type": "enabled" }
},
"fast": {
"long_context": {
"provider": "opencode-go",
"model_id": "deepseek-v4-pro",
"supports_vision": false,
"temperature": 0.1,
"max_tokens": 16384,
"max_output_tokens": 8192,
"context_window": 1000000,
"context_margin": 8192,
"supports_tools": true,
"context_threshold": 80000,
"reasoning_effort": "max",
"thinking": { "type": "enabled" }
},
"vision": {
"provider": "opencode-go",
"model_id": "qwen3.6-plus",
"temperature": 0.7,
"max_tokens": 4096
"supports_vision": true,
"temperature": 0.1,
"max_tokens": 8192,
"max_output_tokens": 8192,
"context_window": 1000000,
"context_margin": 8192,
"supports_tools": true,
"thinking": { "type": "enabled" }
},
"vision_complex": {
"provider": "opencode-go",
"model_id": "kimi-k2.6",
"supports_vision": true,
"temperature": 0.1,
"max_tokens": 8192,
"max_output_tokens": 8192,
"context_window": 256000,
"context_margin": 8192,
"supports_tools": true,
"thinking": { "type": "enabled" }
},
"vision_long_context": {
"provider": "opencode-go",
"model_id": "kimi-k2.6",
"supports_vision": true,
"temperature": 0.1,
"max_tokens": 16384,
"max_output_tokens": 8192,
"context_window": 256000,
"context_margin": 8192,
"supports_tools": true,
"context_threshold": 80000,
"thinking": { "type": "enabled" }
}
},

"fallbacks": {
"background": [
"default": [
{ "provider": "opencode-go", "model_id": "qwen3.6-plus" },
{ "provider": "opencode-go", "model_id": "minimax-m2.5" }
{ "provider": "opencode-go", "model_id": "kimi-k2.6" },
{ "provider": "opencode-go", "model_id": "mimo-v2.5-pro" }
],
"default": [
{ "provider": "opencode-go", "model_id": "mimo-v2-pro" },
"fast": [
{ "provider": "opencode-go", "model_id": "qwen3.5-plus" },
{ "provider": "opencode-go", "model_id": "minimax-m2.5" },
{ "provider": "opencode-go", "model_id": "qwen3.6-plus" }
],
"long_context": [
{ "provider": "opencode-go", "model_id": "minimax-m2.7" },
{ "provider": "opencode-go", "model_id": "kimi-k2.6" }
"background": [
{ "provider": "opencode-go", "model_id": "qwen3.5-plus" },
{ "provider": "opencode-go", "model_id": "minimax-m2.5" },
{ "provider": "opencode-go", "model_id": "qwen3.6-plus" }
],
"think": [
{ "provider": "opencode-go", "model_id": "kimi-k2.6" },
{ "provider": "opencode-go", "model_id": "mimo-v2-pro" }
{ "provider": "opencode-go", "model_id": "mimo-v2.5-pro" },
{ "provider": "opencode-go", "model_id": "glm-5.1" }
],
"complex": [
{ "provider": "opencode-go", "model_id": "glm-5" },
{ "provider": "opencode-go", "model_id": "kimi-k2.6" }
{ "provider": "opencode-go", "model_id": "mimo-v2.5-pro" },
{ "provider": "opencode-go", "model_id": "kimi-k2.6" },
{ "provider": "opencode-go", "model_id": "qwen3.6-plus" }
],
"fast": [
{ "provider": "opencode-go", "model_id": "qwen3.5-plus" },
{ "provider": "opencode-go", "model_id": "minimax-m2.5" }
"long_context": [
{ "provider": "opencode-go", "model_id": "qwen3.6-plus" },
{ "provider": "opencode-go", "model_id": "mimo-v2.5-pro" },
{ "provider": "opencode-go", "model_id": "minimax-m2.7" }
],
"vision": [
{ "provider": "opencode-go", "model_id": "kimi-k2.6", "supports_vision": true },
{ "provider": "opencode-go", "model_id": "qwen3.5-plus", "supports_vision": true },
{ "provider": "opencode-go", "model_id": "kimi-k2.5", "supports_vision": true }
],
"vision_complex": [
{ "provider": "opencode-go", "model_id": "qwen3.6-plus", "supports_vision": true },
{ "provider": "opencode-go", "model_id": "qwen3.5-plus", "supports_vision": true },
{ "provider": "opencode-go", "model_id": "kimi-k2.5", "supports_vision": true }
],
"vision_long_context": [
{ "provider": "opencode-go", "model_id": "kimi-k2.6", "supports_vision": true },
{ "provider": "opencode-go", "model_id": "qwen3.5-plus", "supports_vision": true },
{ "provider": "opencode-go", "model_id": "kimi-k2.5", "supports_vision": true }
]
},

"opencode_go": {
"base_url": "https://opencode.ai/zen/go/v1/chat/completions",
"anthropic_base_url": "https://opencode.ai/zen/go/v1/messages",
"timeout_ms": 300000
},

"logging": {
"level": "info",
"requests": true
Expand Down
18 changes: 18 additions & 0 deletions internal/buildinfo/buildinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package buildinfo

import "os"

var Version = "dev"
var BuildTime = "unknown"

func BinaryPath() string {
path, err := os.Executable()
if err != nil {
return "unknown"
}
return path
}

func PID() int {
return os.Getpid()
}
5 changes: 5 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ type ModelConfig struct {
ModelID string `json:"model_id"`
Temperature float64 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
ContextWindow int `json:"context_window,omitempty"`
ContextMargin int `json:"context_margin,omitempty"`
ContextThreshold int `json:"context_threshold"`
ReasoningEffort string `json:"reasoning_effort"`
SupportsVision bool `json:"supports_vision,omitempty"`
SupportsTools *bool `json:"supports_tools,omitempty"`
Thinking json.RawMessage `json:"thinking,omitempty"`
}

Expand Down
17 changes: 17 additions & 0 deletions internal/config/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,22 @@ func validate(cfg *Config) error {
if cfg.APIKey == "" {
return fmt.Errorf("api_key is required (set via config file or OC_GO_CC_API_KEY env var)")
}
if len(cfg.Models) > 0 {
for _, scenario := range []string{"vision", "vision_complex", "vision_long_context"} {
model, ok := cfg.Models[scenario]
if !ok {
return fmt.Errorf("%s model is required when models are configured", scenario)
}
if !model.SupportsVision {
return fmt.Errorf("%s model %s must support vision", scenario, model.ModelID)
}
for _, fallback := range cfg.Fallbacks[scenario] {
fallback = ResolveModelConfig(fallback)
if !fallback.SupportsVision {
return fmt.Errorf("%s fallback model %s must support vision", scenario, fallback.ModelID)
}
}
}
}
return nil
}
Loading