Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f5f258b
Add Qoder provider support and docs
PKFireBarry Mar 27, 2026
ef1e677
Add Qoder provider support and docs
njuptlzf Apr 27, 2026
f77e84a
feat(qoder): add comprehensive unit tests for Qoder provider
njuptlzf Apr 28, 2026
86bb0b8
fix(qoder): persistent token refresh to correct file path and transla…
njuptlzf Apr 28, 2026
bd8f131
fix(executor): translate Qoder non-stream response to client SourceFo…
njuptlzf Apr 28, 2026
508c9a9
refactor(qoder): clean up dead code and tighten cosy signing
simonsmh May 17, 2026
1a12db5
fix(qoder): fall back to user_id as auth label instead of prompting
simonsmh May 17, 2026
1137aeb
fix(qoder): correct model registry to match upstream identifiers
simonsmh May 17, 2026
efdb6e5
fix(qoder): never prompt for email — derive label deterministically
simonsmh May 17, 2026
9e01129
fix(qoder): tolerate snake/camel split in poll + refresh JSON
simonsmh May 17, 2026
8dec577
fix(qoder): match real /api/v1/deviceToken/poll response shape
simonsmh May 17, 2026
8cb631e
fix(qoder): rewrite COSY signing per Veria, add dynamic model fetch
simonsmh May 17, 2026
72324ba
chore(qoder): bump cosy version 0.14.2 -> 0.2.16, clienttype 0 -> 5
simonsmh May 17, 2026
3d2de6b
fix(qoder): /api/v1/userinfo response is flat, not data-wrapped
simonsmh May 17, 2026
61678c2
fix(qoder): translate stream chunks to client SourceFormat + frame as…
simonsmh May 17, 2026
dab1c2e
refactor(qoder): align stream framing with kimi/openai-compat pattern
simonsmh May 17, 2026
7638c4f
fix(qoder): make Refresh a no-op; device tokens don't OAuth-refresh
simonsmh May 17, 2026
deb9731
fix(qoder): Execute accumulates empty content when SourceFormat is An…
simonsmh May 17, 2026
2e579d7
fix(qoder): align model_config + session_type with Veria for model se…
simonsmh May 17, 2026
5e2aee7
fix(qoder): forward server-provided model_config per-model instead of…
simonsmh May 17, 2026
684f0bd
fix(qoder): error out instead of guessing when model_config is unknown
simonsmh May 17, 2026
77b374d
fix(qoder): flatten multipart content to plain text in chat messages
simonsmh May 17, 2026
09d743f
fix(qoder): pre-translate request before forcing SourceFormat=OpenAI
simonsmh May 17, 2026
818543d
fix(qoder): share TranslateStream param across chunks; remove debug log
simonsmh May 17, 2026
1d7853b
fix(qoder): drop fake tool-call prompt scaffolding; pass tools natively
simonsmh May 17, 2026
a6e9c6c
refactor(qoder): drop "广撒网" duplicate fields, align with Veria minimum
simonsmh May 17, 2026
7181b4b
test(qoder): drop tests for removed messagesToPromptGeneric
simonsmh May 17, 2026
c295e58
refactor(qoder): apply /simplify findings
simonsmh May 17, 2026
ea3b00d
chore(qoder): drop testify + skratchdot deps, rewrite tests in stdlib
simonsmh May 18, 2026
509fd70
docs(qoder): list Qoder after Grok and drop management.html note
simonsmh May 18, 2026
c20cd34
fix(qoder): preserve cached model_configs across auth file reloads
simonsmh May 18, 2026
2297f7e
fix(claude): drop request-body preview from /v1/messages debug log
simonsmh May 18, 2026
a7d129f
chore(qoder): log upstream non-200 metadata for chat requests
simonsmh May 18, 2026
2703981
chore(qoder): apply /simplify cleanups
simonsmh May 19, 2026
8c3b461
fix(qoder): atomic auth file writes and remap 405 to 429
simonsmh May 19, 2026
736756e
chore(qoder): revert non-qoder changes from upstream sync
simonsmh May 19, 2026
ed9d729
fix(qoder): bypass Alibaba Cloud WAF and Qoder upstream rejections
simonsmh May 19, 2026
ba6c554
fix(qoder): remove 405->429 remap now that WAF trigger is fixed
simonsmh May 19, 2026
771e24c
feat(qoder): bypass Alibaba Cloud WAF via body encoding + align reque…
simonsmh May 20, 2026
e17c654
feat(qoder): expose credit usage in management auth list
simonsmh May 20, 2026
c0a9faa
fix(qoder): fix usage JSON tags and context for FetchQoderUsage
simonsmh May 20, 2026
9206755
fix(qoder): remove sensitive debug logs and use proxy-aware client fo…
simonsmh May 20, 2026
75a5814
fix(qoder): preserve system prompts, handle nil content, honor user m…
simonsmh May 20, 2026
4be22cd
fix(qoder): protect UsageInfo with mutex to prevent data race
simonsmh May 20, 2026
b464950
fix(qoder): sanitize debug logs, add Qoder to provider change detection
simonsmh May 21, 2026
9dbfc58
fix(qoder): remove debug log blocks that parsed response field names
simonsmh May 21, 2026
c4123ca
fix(qoder): add empty token check in RefreshTokens
simonsmh May 21, 2026
ed449cd
feat(qoder): add embedded model definitions (11 models)
simonsmh May 21, 2026
7b21c9d
fix(qoder): strip provider prefix from incoming model IDs
simonsmh May 21, 2026
7ca72a0
fix(qoder): add qoder/ prefix to model IDs in FetchQoderModels
simonsmh May 21, 2026
bdc26fa
Merge kaitranntt/main into qoder-provider
simonsmh May 30, 2026
b8f59b7
fix: remove duplicate closing brace in auth_files.go
simonsmh May 30, 2026
03ef3ac
fix(qoder): align executor tests with supported models
kaitranntt May 30, 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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,20 @@ VisionCoder is also offering our users a limited-time <a href="https://coder.vis
- OpenAI Codex support (GPT models) via OAuth login
- Claude Code support via OAuth login
- Grok Build support via OAuth login
- Qoder support via OAuth login
- Amp CLI and IDE extensions support with provider routing
- Streaming, non-streaming, and WebSocket responses where supported
- Function calling/tools support
- Multimodal input support (text and images)
- Multiple accounts with round-robin load balancing (Gemini, OpenAI, Claude, Grok)
- Simple CLI authentication flows (Gemini, OpenAI, Claude, Grok)
- Multiple accounts with round-robin load balancing (Gemini, OpenAI, Claude, Grok, Qoder)
- Simple CLI authentication flows (Gemini, OpenAI, Claude, Grok, Qoder)
- Generative Language API Key support
- AI Studio Build multi-account load balancing
- Gemini CLI multi-account load balancing
- Claude Code multi-account load balancing
- OpenAI Codex multi-account load balancing
- Grok Build multi-account load balancing
- Qoder multi-account load balancing
- OpenAI-compatible upstream providers via config (e.g., OpenRouter)
- Reusable Go SDK for embedding the proxy (see `docs/sdk-usage.md`)

Expand Down
6 changes: 4 additions & 2 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,19 @@ VisionCoder 还为我们的用户提供 <a href="https://coder.visioncoder.cn" t
- 新增 OpenAI Codex(GPT 系列)支持(OAuth 登录)
- 新增 Claude Code 支持(OAuth 登录)
- 新增 Grok Build 支持(OAuth 登录)
- 新增 Qoder 支持(OAuth 登录)
- 支持流式、非流式响应,以及受支持场景下的 WebSocket 响应
- 函数调用/工具支持
- 多模态输入(文本、图片)
- 多账户支持与轮询负载均衡(Gemini、OpenAI、Claude、Grok)
- 简单的 CLI 身份验证流程(Gemini、OpenAI、Claude、Grok)
- 多账户支持与轮询负载均衡(Gemini、OpenAI、Claude、Qwen、Grok、Qoder 与 iFlow
- 简单的 CLI 身份验证流程(Gemini、OpenAI、Claude、Qwen、Grok、Qoder 与 iFlow
- 支持 Gemini AIStudio API 密钥
- 支持 AI Studio Build 多账户轮询
- 支持 Gemini CLI 多账户轮询
- 支持 Claude Code 多账户轮询
- 支持 OpenAI Codex 多账户轮询
- 支持 Grok Build 多账户轮询
- 支持 Qoder 多账户轮询
- 通过配置接入上游 OpenAI 兼容提供商(例如 OpenRouter)
- 可复用的 Go SDK(见 `docs/sdk-usage_CN.md`)

Expand Down
6 changes: 4 additions & 2 deletions README_JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,20 @@ PackyCodeは当ソフトウェアのユーザーに特別割引を提供して
- OAuthログインによるOpenAI Codexサポート(GPTモデル)
- OAuthログインによるClaude Codeサポート
- OAuthログインによるGrok Buildサポート
- OAuthログインによるQoderサポート
- プロバイダールーティングによるAmp CLIおよびIDE拡張機能のサポート
- ストリーミング、非ストリーミング、および対応環境でのWebSocketレスポンス
- 関数呼び出し/ツールのサポート
- マルチモーダル入力サポート(テキストと画像)
- ラウンドロビン負荷分散による複数アカウント対応(Gemini、OpenAI、Claude、Grok)
- シンプルなCLI認証フロー(Gemini、OpenAI、Claude、Grok)
- ラウンドロビン負荷分散による複数アカウント対応(Gemini、OpenAI、Claude、Qwen、Grok、QoderおよびiFlow
- シンプルなCLI認証フロー(Gemini、OpenAI、Claude、Qwen、Grok、QoderおよびiFlow
- Generative Language APIキーのサポート
- AI Studioビルドのマルチアカウント負荷分散
- Gemini CLIのマルチアカウント負荷分散
- Claude Codeのマルチアカウント負荷分散
- OpenAI Codexのマルチアカウント負荷分散
- Grok Buildのマルチアカウント負荷分散
- Qoderのマルチアカウント負荷分散
- 設定によるOpenAI互換アップストリームプロバイダー(例:OpenRouter)
- プロキシ埋め込み用の再利用可能なGo SDK(`docs/sdk-usage.md`を参照)

Expand Down
225 changes: 225 additions & 0 deletions cmd/qoder_replay/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// Command qoder_replay replays a captured Qoder request JSON file against the
// live upstream, using stored credentials for COSY signing. Supports binary
// search mode to isolate the message that triggers a WAF 405.
//
// Usage:
//
// qoder_replay -auth <auth-file> -req <req-file>
// qoder_replay -auth <auth-file> -req <req-file> -bisect
package main

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"

"github.com/google/uuid"
qoderauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/qoder"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
)

func main() {
authFile := flag.String("auth", "", "Path to qoder auth JSON file")
reqFile := flag.String("req", "", "Path to captured request JSON file")
bisect := flag.Bool("bisect", false, "Binary search for the offending message")
chatURL := flag.String("url", "", "Override chat URL (default: auto based on -encode flag)")
encode := flag.Bool("encode", true, "Wrap and encode body (Encode=1 mode)")
flag.Parse()

if *chatURL == "" {
if *encode {
*chatURL = qoderauth.QoderChatURLEncoded
} else {
*chatURL = qoderauth.QoderChatURL
}
}

if *authFile == "" || *reqFile == "" {
fmt.Fprintln(os.Stderr, "usage: qoder_replay -auth <auth-file> -req <req-file> [-bisect]")
os.Exit(1)
}

// Load auth
authData, err := os.ReadFile(*authFile)
if err != nil {
fatalf("read auth: %v", err)
}
var storage qoderauth.QoderTokenStorage
if err := json.Unmarshal(authData, &storage); err != nil {
fatalf("parse auth: %v", err)
}

// Load request
reqData, err := os.ReadFile(*reqFile)
if err != nil {
fatalf("read req: %v", err)
}
var reqBody map[string]interface{}
if err := json.Unmarshal(reqData, &reqBody); err != nil {
fatalf("parse req: %v", err)
}

msgs, _ := reqBody["messages"].([]interface{})
fmt.Printf("Loaded request with %d messages\n", len(msgs))

if !*bisect {
status, body := send(&storage, reqBody, *chatURL, *encode)
fmt.Printf("Status: %d\n", status)
fmt.Printf("Body: %s\n", truncate(string(body), 500))
return
}

// Binary search
fmt.Printf("\nBisecting %d messages...\n\n", len(msgs))

// Verify full set fails
status, _ := send(&storage, withMessages(reqBody, msgs), *chatURL, *encode)
if status != 405 {
fmt.Printf("WARNING: full request returned %d (not 405), bisect may be unreliable\n", status)
}

lo, hi := 0, len(msgs)
for hi-lo > 1 {
mid := (lo + hi) / 2
subset := msgs[:mid]
fmt.Printf("Testing msgs[0:%d]... ", mid)
status, _ := send(&storage, withMessages(reqBody, subset), *chatURL, *encode)
fmt.Printf("-> %d\n", status)
if status == 405 {
hi = mid
} else {
lo = mid
}
}

fmt.Printf("\nOffending message index: %d\n", lo)
m, _ := msgs[lo].(map[string]interface{})
if m == nil {
fmt.Println("(could not parse message)")
return
}
fmt.Printf("role: %s\n", m["role"])
content, _ := m["content"].(string)
fmt.Printf("content: %s\n", truncate(content, 300))
if tcs, ok := m["tool_calls"].([]interface{}); ok {
for _, tc := range tcs {
tcMap, _ := tc.(map[string]interface{})
if tcMap == nil {
continue
}
fn, _ := tcMap["function"].(map[string]interface{})
if fn == nil {
continue
}
fmt.Printf("tool_call: %s\n args: %s\n", fn["name"], truncate(fmt.Sprintf("%v", fn["arguments"]), 500))
}
}
}

func withMessages(base map[string]interface{}, msgs []interface{}) map[string]interface{} {
clone := make(map[string]interface{}, len(base))
for k, v := range base {
clone[k] = v
}
clone["messages"] = msgs
// Fresh IDs so each bisect call is independent
clone["request_id"] = uuid.New().String()
clone["request_set_id"] = uuid.New().String()
clone["chat_record_id"] = uuid.New().String()
clone["session_id"] = uuid.New().String()
if biz, ok := clone["business"].(map[string]interface{}); ok {
bizClone := make(map[string]interface{}, len(biz))
for k, v := range biz {
bizClone[k] = v
}
bizClone["id"] = uuid.New().String()
bizClone["begin_at"] = time.Now().UnixMilli()
clone["business"] = bizClone
}
return clone
}

func send(storage *qoderauth.QoderTokenStorage, reqBody map[string]interface{}, chatURL string, encode bool) (int, []byte) {
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
fatalf("marshal: %v", err)
}

var sendBytes []byte
if encode {
sendBytes = []byte(helps.QoderEncodeBody(bodyBytes))
} else {
sendBytes = bodyBytes
}

req, err := http.NewRequest("POST", chatURL, bytes.NewReader(sendBytes))
if err != nil {
fatalf("new request: %v", err)
}

headers, err := qoderauth.BuildAuthHeaders(
sendBytes,
chatURL,
qoderauth.CosyCredentials{
UserID: storage.UserID,
AuthToken: storage.Token,
Name: storage.Name,
Email: storage.Email,
MachineID: storage.MachineID,
},
)
if err != nil {
fatalf("build auth headers: %v", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Cache-Control", "no-cache")
headers.Apply(req)
// Extract model key from request body for X-Model-Key header
if modelKey, ok := reqBody["model_config"].(map[string]interface{}); ok {
if key, ok := modelKey["key"].(string); ok && key != "" {
req.Header.Set("X-Model-Key", key)
}
if src, ok := modelKey["source"].(string); ok && src != "" {
req.Header.Set("X-Model-Source", src)
} else {
req.Header.Set("X-Model-Source", "system")
}
}
req.Header.Set("Accept-Encoding", "identity")

client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, "request error: %v\n", err)
return 0, nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return resp.StatusCode, body
}
// For 200 SSE responses, just read the first chunk to confirm success.
buf := make([]byte, 512)
n, _ := resp.Body.Read(buf)
return resp.StatusCode, buf[:n]
}

func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}

func fatalf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "error: "+format+"\n", args...)
os.Exit(1)
}
4 changes: 4 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func main() {
var githubCopilotLogin bool
var codeBuddyLogin bool
var xaiLogin bool
var qoderLogin bool
var projectID string
var vertexImport string
var vertexImportPrefix string
Expand Down Expand Up @@ -141,6 +142,7 @@ func main() {
flag.BoolVar(&githubCopilotLogin, "github-copilot-login", false, "Login to GitHub Copilot using device flow")
flag.BoolVar(&codeBuddyLogin, "codebuddy-login", false, "Login to CodeBuddy using browser OAuth flow")
flag.BoolVar(&xaiLogin, "xai-login", false, "Login to xAI using OAuth")
flag.BoolVar(&qoderLogin, "qoder-login", false, "Login to Qoder using OAuth device flow")
flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
flag.StringVar(&configPath, "config", DefaultConfigPath, "Configure File Path")
flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file")
Expand Down Expand Up @@ -680,6 +682,8 @@ func main() {
cmd.DoKiroIDCLogin(cfg, options, kiroIDCStartURL, kiroIDCRegion, kiroIDCFlow)
} else if xaiLogin {
cmd.DoXAILogin(cfg, options)
} else if qoderLogin {
cmd.DoQoderLogin(cfg, options)
} else {
// In cloud deploy mode without config file, just wait for shutdown signals
if isCloudDeploy && !configFileExists {
Expand Down
9 changes: 7 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ nonstream-keepalive-interval: 0

# Global OAuth model name aliases (per channel)
# These aliases rename model IDs for both model listing and request routing.
# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, kimi, xai.
# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, kimi, xai, qoder.
# NOTE: Aliases do not apply to gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, or ampcode.
# NOTE: Because aliases affect the merged /v1 model list and merged request routing, overlapping
# client-visible names can become ambiguous across providers. /api/provider/{provider}/... helps
Expand Down Expand Up @@ -444,9 +444,12 @@ nonstream-keepalive-interval: 0
# xai:
# - name: "grok-4.3"
# alias: "grok-latest"
# qoder:
# - name: "auto"
# alias: "qoder-auto"

# OAuth provider excluded models
# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow, kiro, github-copilot.
# Supported channels: gemini-cli, vertex, aistudio, antigravity, claude, codex, qwen, iflow, kiro, github-copilot, qoder.
# oauth-excluded-models:
# gemini-cli:
# - "gemini-2.5-pro" # exclude specific models (exact match)
Expand All @@ -467,6 +470,8 @@ nonstream-keepalive-interval: 0
# - "kimi-k2-thinking"
# xai:
# - "grok-3-mini"
# qoder:
# - "auto"

# Optional payload configuration
# payload:
Expand Down
Loading
Loading