Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39c415ebaa | |||
|
|
643efdabb6 | ||
| 1b685002b8 | |||
| db1dd25a9b | |||
|
|
7fdcf01534 | ||
|
|
582913dfcc | ||
|
|
23ebb2b09f |
3 changed files with 246 additions and 68 deletions
2
Makefile
2
Makefile
|
|
@ -5,7 +5,7 @@ EXE := $(shell go env GOEXE)
|
|||
OCGO_BIN := bin/ocgo$(EXE)
|
||||
GOBIN := $(shell go env GOBIN)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
INSTALL_DIR := $(if $(GOBIN),$(GOBIN),$(GOPATH)/bin)
|
||||
INSTALL_DIR := $(HOME)/.local/bin
|
||||
|
||||
build:
|
||||
go build -ldflags "-X main.version=$(VERSION)" -o $(OCGO_BIN) ./cmd/ocgo
|
||||
|
|
|
|||
266
cmd/ocgo/main.go
266
cmd/ocgo/main.go
|
|
@ -32,9 +32,11 @@ const (
|
|||
var version = "dev"
|
||||
|
||||
type Config struct {
|
||||
APIKey string `json:"api_key"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
APIKeys []string `json:"api_keys,omitempty"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
type AnthropicRequest struct {
|
||||
|
|
@ -147,30 +149,38 @@ func main() {
|
|||
}
|
||||
|
||||
func setupCmd() *cobra.Command {
|
||||
var key string
|
||||
var apiKeys string
|
||||
cmd := &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Save your OpenCode Go API key",
|
||||
Short: "Save your OpenCode Go API key(s)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if strings.TrimSpace(key) == "" {
|
||||
key = os.Getenv("OCGO_API_KEY")
|
||||
if apiKeys == "" {
|
||||
apiKeys, _ = cmd.Flags().GetString("api-key")
|
||||
}
|
||||
if strings.TrimSpace(key) == "" {
|
||||
fmt.Print("OpenCode Go API key: ")
|
||||
if strings.TrimSpace(apiKeys) == "" {
|
||||
apiKeys = os.Getenv("OCGO_API_KEYS")
|
||||
}
|
||||
if strings.TrimSpace(apiKeys) == "" {
|
||||
apiKeys = os.Getenv("OCGO_API_KEY")
|
||||
}
|
||||
if strings.TrimSpace(apiKeys) == "" {
|
||||
fmt.Print("OpenCode Go API key(s) (comma-separated): ")
|
||||
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if err != nil && line == "" {
|
||||
return err
|
||||
}
|
||||
key = line
|
||||
apiKeys = line
|
||||
}
|
||||
cfg := Config{APIKey: strings.TrimSpace(key), Host: defaultHost, Port: defaultPort}
|
||||
if cfg.APIKey == "" {
|
||||
return errors.New("API key cannot be empty")
|
||||
keys := parseKeys(strings.TrimSpace(apiKeys))
|
||||
if len(keys) == 0 {
|
||||
return errors.New("at least one API key is required")
|
||||
}
|
||||
cfg := Config{APIKeys: keys, Host: defaultHost, Port: defaultPort}
|
||||
return saveConfig(cfg)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&key, "api-key", "", "OpenCode Go API key")
|
||||
cmd.Flags().StringVar(&apiKeys, "api-keys", "", "OpenCode Go API key(s), comma-separated")
|
||||
cmd.Flags().String("api-key", "", "OpenCode Go API key (single)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +194,39 @@ func listCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
func knownModelIDs() []string {
|
||||
return []string{"glm-5.1", "glm-5", "kimi-k2.6", "kimi-k2.5", "mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro", "mimo-v2-omni", "minimax-m2.7", "minimax-m2.5", "deepseek-v4-pro", "deepseek-v4-flash", "qwen3.6-plus", "qwen3.5-plus"}
|
||||
return []string{"glm-5.1", "glm-5", "kimi-k2.6", "kimi-k2.5", "mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro", "mimo-v2-omni", "minimax-m2.7", "minimax-m2.5", "deepseek-v4-pro[1M]", "deepseek-v4-flash[1M]", "qwen3.6-plus", "qwen3.5-plus"}
|
||||
}
|
||||
|
||||
func isKnownModelID(model string) bool {
|
||||
cleaned := cleanModelName(model)
|
||||
for _, m := range knownModelIDs() {
|
||||
if cleanModelName(m) == cleaned {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func cleanModelName(model string) string {
|
||||
model = strings.TrimSuffix(model, "[1M]")
|
||||
model = strings.TrimSuffix(model, "[128K]")
|
||||
return model
|
||||
}
|
||||
|
||||
func modelContextWindow(model string) int {
|
||||
switch cleanModelName(model) {
|
||||
case "deepseek-v4-pro", "deepseek-v4-flash":
|
||||
return 1000000
|
||||
default:
|
||||
return 128000
|
||||
}
|
||||
}
|
||||
|
||||
func modelDisplayName(model string) string {
|
||||
if modelContextWindow(model) >= 1000000 {
|
||||
return model + "[1M]"
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
func modelSupportsImages(model string) bool {
|
||||
|
|
@ -284,17 +326,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
|
||||
}
|
||||
|
||||
|
|
@ -369,20 +416,13 @@ 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
|
||||
}
|
||||
body, _ := json.Marshal(or)
|
||||
req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, openAIURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := (&http.Client{Timeout: 10 * time.Minute}).Do(req)
|
||||
resp, err := cfg.postWithRetry(r.Context(), openAIURL, "application/json", body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
|
|
@ -394,10 +434,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) {
|
||||
|
|
@ -415,14 +455,18 @@ func proxyChatCompletions(w http.ResponseWriter, r *http.Request, cfg Config) {
|
|||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, openAIURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
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 == "" || !isKnownModelID(m) {
|
||||
reqMap["model"] = dm
|
||||
body, _ = json.Marshal(reqMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := (&http.Client{Timeout: 10 * time.Minute}).Do(req)
|
||||
resp, err := cfg.postWithRetry(r.Context(), openAIURL, "application/json", body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
|
|
@ -443,20 +487,13 @@ 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
|
||||
}
|
||||
body, _ := json.Marshal(or)
|
||||
req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, openAIURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := (&http.Client{Timeout: 10 * time.Minute}).Do(req)
|
||||
resp, err := cfg.postWithRetry(r.Context(), openAIURL, "application/json", body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
|
|
@ -468,10 +505,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) {
|
||||
|
|
@ -489,6 +526,11 @@ func prepareChatBody(body []byte) ([]byte, error) {
|
|||
}
|
||||
changed := requestStreamingUsage(req)
|
||||
model, _ := req["model"].(string)
|
||||
if clean := cleanModelName(model); clean != model {
|
||||
req["model"] = clean
|
||||
changed = true
|
||||
model = clean
|
||||
}
|
||||
if rawChatBodyHasImages(req) {
|
||||
if !modelSupportsImages(model) {
|
||||
return nil, unsupportedImageModelError(model)
|
||||
|
|
@ -581,9 +623,12 @@ func stripRawChatImageDetails(req map[string]any) bool {
|
|||
return changed
|
||||
}
|
||||
|
||||
func convertRequest(ar AnthropicRequest) OAIRequest {
|
||||
model := ar.Model
|
||||
if model == "" || strings.HasPrefix(model, "claude-") {
|
||||
func convertRequest(ar AnthropicRequest, defaultModel string) OAIRequest {
|
||||
model := cleanModelName(ar.Model)
|
||||
if model == "" || !isKnownModelID(model) {
|
||||
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}
|
||||
|
|
@ -599,8 +644,11 @@ func convertRequest(ar AnthropicRequest) OAIRequest {
|
|||
return out
|
||||
}
|
||||
|
||||
func responsesToChat(rr ResponsesRequest) OAIRequest {
|
||||
model := rr.Model
|
||||
func responsesToChat(rr ResponsesRequest, defaultModel string) OAIRequest {
|
||||
model := cleanModelName(rr.Model)
|
||||
if model == "" || !isKnownModelID(model) {
|
||||
model = cleanModelName(defaultModel)
|
||||
}
|
||||
if model == "" {
|
||||
model = "kimi-k2.6"
|
||||
}
|
||||
|
|
@ -1372,7 +1420,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)
|
||||
|
|
@ -1390,7 +1438,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
|
||||
}
|
||||
|
|
@ -1425,12 +1473,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
|
||||
|
|
@ -1439,6 +1487,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 {
|
||||
|
|
@ -1459,6 +1510,76 @@ func startServerProcess(detached bool) (*exec.Cmd, error) {
|
|||
func configDir() string { home, _ := os.UserHomeDir(); return filepath.Join(home, ".config", "ocgo") }
|
||||
func configFile() string { return filepath.Join(configDir(), "config.json") }
|
||||
func pidFile() string { return filepath.Join(configDir(), "ocgo.pid") }
|
||||
func keyIndexFile() string { return filepath.Join(configDir(), "key-index") }
|
||||
|
||||
func readKeyIndex() int {
|
||||
b, err := os.ReadFile(keyIndexFile())
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
var idx int
|
||||
fmt.Sscanf(string(b), "%d", &idx)
|
||||
return idx
|
||||
}
|
||||
|
||||
func writeKeyIndex(idx int) error {
|
||||
return os.WriteFile(keyIndexFile(), []byte(fmt.Sprint(idx)), 0600)
|
||||
}
|
||||
|
||||
func (cfg Config) activeKeys() []string {
|
||||
if len(cfg.APIKeys) > 0 {
|
||||
return cfg.APIKeys
|
||||
}
|
||||
if cfg.APIKey != "" {
|
||||
return []string{cfg.APIKey}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg Config) currentKey() string {
|
||||
keys := cfg.activeKeys()
|
||||
if len(keys) == 0 {
|
||||
return ""
|
||||
}
|
||||
idx := readKeyIndex() % len(keys)
|
||||
return keys[idx]
|
||||
}
|
||||
|
||||
func (cfg Config) rotateKey() string {
|
||||
keys := cfg.activeKeys()
|
||||
if len(keys) <= 1 {
|
||||
return cfg.currentKey()
|
||||
}
|
||||
idx := readKeyIndex()
|
||||
idx = (idx + 1) % len(keys)
|
||||
_ = writeKeyIndex(idx)
|
||||
return keys[idx]
|
||||
}
|
||||
|
||||
func (cfg Config) postWithRetry(ctx context.Context, url, contentType string, body []byte) (*http.Response, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Minute}
|
||||
keys := cfg.activeKeys()
|
||||
for i := 0; i < len(keys); i++ {
|
||||
key := cfg.currentKey()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode == http.StatusTooManyRequests && i < len(keys)-1 {
|
||||
resp.Body.Close()
|
||||
cfg.rotateKey()
|
||||
continue
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func codexConfigFile() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
|
|
@ -1522,6 +1643,7 @@ func writeCodexProfile(path, baseURL string) error {
|
|||
func writeCodexModelCatalog(path string) error {
|
||||
models := make([]map[string]any, 0, len(knownModelIDs()))
|
||||
for i, id := range knownModelIDs() {
|
||||
cw := modelContextWindow(id)
|
||||
models = append(models, map[string]any{
|
||||
"slug": id,
|
||||
"display_name": id,
|
||||
|
|
@ -1544,8 +1666,8 @@ func writeCodexModelCatalog(path string) error {
|
|||
"truncation_policy": map[string]any{"mode": "tokens", "limit": 10000},
|
||||
"supports_parallel_tool_calls": false,
|
||||
"supports_image_detail_original": false,
|
||||
"context_window": 128000,
|
||||
"max_context_window": 128000,
|
||||
"context_window": cw,
|
||||
"max_context_window": cw,
|
||||
"auto_compact_token_limit": nil,
|
||||
"effective_context_window_percent": 95,
|
||||
"experimental_supported_tools": []any{},
|
||||
|
|
@ -1610,6 +1732,12 @@ func versionParts(v string) [3]int {
|
|||
}
|
||||
|
||||
func saveConfig(cfg Config) error {
|
||||
// Write api_key for backward compatibility with older ocgo versions
|
||||
if len(cfg.APIKeys) > 0 {
|
||||
cfg.APIKey = cfg.APIKeys[0]
|
||||
} else {
|
||||
cfg.APIKey = ""
|
||||
}
|
||||
if err := os.MkdirAll(configDir(), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -1622,12 +1750,21 @@ func saveConfig(cfg Config) error {
|
|||
}
|
||||
|
||||
func loadConfig() (Config, error) {
|
||||
cfg := Config{Host: defaultHost, Port: defaultPort, APIKey: os.Getenv("OCGO_API_KEY")}
|
||||
cfg := Config{Host: defaultHost, Port: defaultPort}
|
||||
b, err := os.ReadFile(configFile())
|
||||
if err == nil {
|
||||
_ = json.Unmarshal(b, &cfg)
|
||||
}
|
||||
if cfg.APIKey == "" {
|
||||
if envKey := os.Getenv("OCGO_API_KEY"); envKey != "" {
|
||||
cfg.APIKey = envKey
|
||||
}
|
||||
if envKeys := os.Getenv("OCGO_API_KEYS"); envKeys != "" {
|
||||
cfg.APIKeys = parseKeys(envKeys)
|
||||
}
|
||||
if len(cfg.APIKeys) == 0 && cfg.APIKey != "" {
|
||||
cfg.APIKeys = []string{cfg.APIKey}
|
||||
}
|
||||
if len(cfg.APIKeys) == 0 {
|
||||
return cfg, errors.New("missing API key; run: ocgo setup")
|
||||
}
|
||||
if cfg.Host == "" {
|
||||
|
|
@ -1639,6 +1776,17 @@ func loadConfig() (Config, error) {
|
|||
return cfg, nil
|
||||
}
|
||||
|
||||
func parseKeys(s string) []string {
|
||||
parts := strings.Split(s, ",")
|
||||
var keys []string
|
||||
for _, p := range parts {
|
||||
if k := strings.TrimSpace(p); k != "" {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func readPID() (int, error) {
|
||||
b, err := os.ReadFile(pidFile())
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,36 @@ func TestWriteCodexProfileReplacesExistingSections(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestModelContextWindow(t *testing.T) {
|
||||
if got := modelContextWindow("deepseek-v4-flash[1M]"); got != 1000000 {
|
||||
t.Fatalf("deepseek-v4-flash[1M]: got %d, want 1000000", got)
|
||||
}
|
||||
if got := modelContextWindow("deepseek-v4-pro[1M]"); got != 1000000 {
|
||||
t.Fatalf("deepseek-v4-pro[1M]: got %d, want 1000000", got)
|
||||
}
|
||||
if got := modelContextWindow("kimi-k2.6"); got != 128000 {
|
||||
t.Fatalf("kimi-k2.6: got %d, want 128000", got)
|
||||
}
|
||||
if got := modelContextWindow("unknown-model"); got != 128000 {
|
||||
t.Fatalf("unknown-model: got %d, want 128000", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanModelName(t *testing.T) {
|
||||
tests := []struct{ in, want string }{
|
||||
{"deepseek-v4-flash[1M]", "deepseek-v4-flash"},
|
||||
{"deepseek-v4-pro[1M]", "deepseek-v4-pro"},
|
||||
{"deepseek-v4-flash", "deepseek-v4-flash"},
|
||||
{"kimi-k2.6", "kimi-k2.6"},
|
||||
{"kimi-k2.6[128K]", "kimi-k2.6"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := cleanModelName(tt.in); got != tt.want {
|
||||
t.Errorf("cleanModelName(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCodexModelCatalog(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "ocgo-models.json")
|
||||
if err := writeCodexModelCatalog(path); err != nil {
|
||||
|
|
@ -70,7 +100,7 @@ func TestWriteCodexModelCatalog(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
content := string(b)
|
||||
for _, want := range []string{`"models"`, `"slug": "deepseek-v4-pro"`, `"context_window": 128000`, `"truncation_policy"`, `"supports_image_detail_original": false`, `"image"`} {
|
||||
for _, want := range []string{`"models"`, `"slug": "deepseek-v4-pro[1M]"`, `"context_window": 1000000`, `"truncation_policy"`, `"supports_image_detail_original": false`, `"image"`} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("missing %q in:\n%s", want, content)
|
||||
}
|
||||
|
|
@ -187,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)
|
||||
}
|
||||
|
|
@ -198,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)
|
||||
}
|
||||
|
|
@ -243,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)
|
||||
}
|
||||
|
|
@ -275,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)
|
||||
}
|
||||
|
|
@ -285,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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue