mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 12:17:23 +00:00
chore!: 移除厂商私有协议适配器与渠道业务插件(合规清理)
G-Star 合规要求下,把"模拟官方客户端、复刻私有协议、自动领取平台额度"这一类 能力整体移出仓库。这些实现依赖各平台未公开的端点、签名算法与官方 client id, 属于绕过客户端校验获取额度,风险高于普通协议适配。 移除(93 个文件): - 适配器:opencodezen(伪装 UA 取免费池)、trae、buddy、kimicode - 插件:channels.lua(12 个平台登录/领额度/签到)、quota.lua(厂商额度探针) - 随之删除的 78 个渠道/额度专用测试与 3 份渠道文档 同时修掉的既有问题: - **硬编码第三方 OAuth client_secret**(Google Cloud Code 的 GOCSPX-…,两处) 随 channels.lua 一起消失。 - `deploy.sh` 的 `sync_plugins` 整段删除:它仍按"内置插件是发行物"的旧前提 把插件复制/覆盖进 plugin_dir,与"插件属运维资产、网关不安装不覆盖"的契约 冲突。现在部署脚本只同步适配器与二进制/配置。 - 适配器清单去掉已移除项;e2e 配置改用显式 `auto:` 链(`models.priority` 早已 移除,该测试此前已失效)。 保留(公开协议):openai / anthropic / gemini / deepseek / github / groq / mistral / ollama / qwen38 / sensenova = 10 个适配器;插件示例 billing / usage。 验证:go build -tags luajit ./... OK;internal/lua 21.4s、provider、core 51s、 gateway 122.8s、e2e 9.0s 全绿。
This commit is contained in:
@ -32,7 +32,7 @@ func TestLoadBundledAdapters(t *testing.T) {
|
||||
for _, a := range adapters {
|
||||
names[a.Name] = true
|
||||
}
|
||||
for _, want := range []string{"openai", "deepseek", "anthropic", "gemini", "ollama", "kimicode"} {
|
||||
for _, want := range []string{"openai", "deepseek", "anthropic", "gemini", "ollama"} {
|
||||
if !names[want] {
|
||||
t.Errorf("missing adapter %s (got %v)", want, names)
|
||||
}
|
||||
@ -115,36 +115,6 @@ func TestBuildHeadersFallbackStatic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHeadersCustomHook(t *testing.T) {
|
||||
vm := NewVM(freshAdapterDir(t))
|
||||
if err := vm.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer vm.Stop()
|
||||
hdrs, err := vm.BuildHeaders("kimicode", map[string]interface{}{
|
||||
"timestamp": int64(12345),
|
||||
"api_key": "k",
|
||||
"body": "{}",
|
||||
"method": "POST",
|
||||
"url": "http://x/chat",
|
||||
"source": map[string]interface{}{"meta": map[string]interface{}{
|
||||
"app_id": "app-9", "app_secret": "s", "api_key": "k",
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("build headers: %v", err)
|
||||
}
|
||||
if hdrs["X-App-Id"] != "app-9" {
|
||||
t.Fatalf("x-app-id = %q", hdrs["X-App-Id"])
|
||||
}
|
||||
if hdrs["X-App-Sign"] == "" {
|
||||
t.Fatal("expected signature header")
|
||||
}
|
||||
if hdrs["X-Timestamp"] != "12345" {
|
||||
t.Fatalf("timestamp = %q", hdrs["X-Timestamp"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableThinkingPassthrough(t *testing.T) {
|
||||
vm := NewVM(freshAdapterDir(t))
|
||||
if err := vm.Start(); err != nil {
|
||||
@ -296,7 +266,7 @@ func TestAdaptersPassFinishReason(t *testing.T) {
|
||||
// every chunk) must NOT terminate the stream.
|
||||
chunk := `{"choices":[{"index":0,"finish_reason":"tool_calls","delta":{"content":""}}]}`
|
||||
empty := `{"choices":[{"index":0,"finish_reason":"","delta":{"content":"x"}}]}`
|
||||
for _, name := range []string{"openai", "deepseek", "github", "groq", "kimicode", "mistral"} {
|
||||
for _, name := range []string{"openai", "deepseek", "github", "groq", "mistral"} {
|
||||
out, err := vm.Transform(name, "transform_stream_chunk", chunk)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
@ -521,7 +491,7 @@ func TestAdaptersReportZeroCacheHit(t *testing.T) {
|
||||
defer vm.Stop()
|
||||
|
||||
// OpenAI-shaped upstreams: usage.prompt_tokens_details.cached_tokens = 0
|
||||
openaiLike := []string{"openai", "deepseek", "sensenova", "github", "groq", "kimicode", "mistral"}
|
||||
openaiLike := []string{"openai", "deepseek", "sensenova", "github", "groq", "mistral"}
|
||||
respBody := `{"choices":[{"message":{"content":"hi"},"finish_reason":"stop"}],
|
||||
"usage":{"prompt_tokens":10,"completion_tokens":2,"total_tokens":12,
|
||||
"prompt_tokens_details":{"cached_tokens":0}}}`
|
||||
@ -1248,8 +1218,8 @@ func TestNonStreamToolCallsPreserved(t *testing.T) {
|
||||
`"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}}`
|
||||
|
||||
for _, name := range []string{
|
||||
"openai", "sensenova", "trae", "deepseek", "github",
|
||||
"groq", "kimicode", "mistral",
|
||||
"openai", "sensenova", "deepseek", "github",
|
||||
"groq", "mistral",
|
||||
} {
|
||||
out, err := vm.Transform(name, "transform_response", body)
|
||||
if err != nil {
|
||||
@ -1369,86 +1339,6 @@ func TestOllamaNonStreamToolCalls(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraeTextToolCallRecovery covers the trae-specific failure: its upstream
|
||||
// (trae-local-api's OpenAI endpoint) never reads the request's `tools` array, so
|
||||
// the relayed model prints a <tool_call> block as TEXT, leaves
|
||||
// message.tool_calls null and reports finish_reason:"stop". An OpenAI client
|
||||
// then sees an ordinary completion and its agent loop ends mid-conversation.
|
||||
// The adapter must recover the structured call, strip the block from content,
|
||||
// and correct finish_reason.
|
||||
func TestTraeTextToolCallRecovery(t *testing.T) {
|
||||
vm := NewVM(freshAdapterDir(t))
|
||||
if err := vm.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer vm.Stop()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{
|
||||
"tool_call tag with arguments",
|
||||
`{"choices":[{"message":{"role":"assistant","content":"<tool_call>\n{\"name\": \"get_weather\", \"arguments\": {\"city\": \"Beijing\"}}\n</tool_call>"},"finish_reason":"stop"}]}`,
|
||||
},
|
||||
{
|
||||
// the same codebase's Anthropic endpoint prompts for <toolcall> and "params"
|
||||
"toolcall tag with params",
|
||||
`{"choices":[{"message":{"role":"assistant","content":"<toolcall>{\"name\": \"get_weather\", \"params\": {\"city\": \"Beijing\"}}</toolcall>"},"finish_reason":"stop"}]}`,
|
||||
},
|
||||
{
|
||||
"prose around the block is preserved",
|
||||
`{"choices":[{"message":{"role":"assistant","content":"Let me check.\n<tool_call>{\"name\":\"get_weather\",\"arguments\":{\"city\":\"Beijing\"}}</tool_call>"},"finish_reason":"stop"}]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
out, err := vm.Transform("trae", "transform_response", c.body)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: transform: %v", c.name, err)
|
||||
}
|
||||
var got struct {
|
||||
Content string `json:"content"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
ToolCalls []struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
} `json:"tool_calls"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &got); err != nil {
|
||||
t.Fatalf("%s: unmarshal %v (%s)", c.name, err, out)
|
||||
}
|
||||
if len(got.ToolCalls) != 1 {
|
||||
t.Fatalf("%s: text tool call not recovered: %s", c.name, out)
|
||||
}
|
||||
if got.ToolCalls[0].Name != "get_weather" {
|
||||
t.Errorf("%s: name wrong: %s", c.name, out)
|
||||
}
|
||||
if got.ToolCalls[0].Arguments["city"] != "Beijing" {
|
||||
t.Errorf("%s: args wrong: %s", c.name, out)
|
||||
}
|
||||
if got.FinishReason != "tool_calls" {
|
||||
t.Errorf("%s: finish_reason must be corrected to tool_calls: %s", c.name, out)
|
||||
}
|
||||
if strings.Contains(got.Content, "<tool_call") || strings.Contains(got.Content, "<toolcall") {
|
||||
t.Errorf("%s: raw block left in user-visible content: %s", c.name, out)
|
||||
}
|
||||
}
|
||||
|
||||
// a plain text answer must be untouched
|
||||
plain := `{"choices":[{"message":{"role":"assistant","content":"just text"},"finish_reason":"stop"}]}`
|
||||
out, err := vm.Transform("trae", "transform_response", plain)
|
||||
if err != nil {
|
||||
t.Fatalf("plain: %v", err)
|
||||
}
|
||||
if strings.Contains(out, "tool_calls") {
|
||||
t.Errorf("plain answer must not gain tool_calls: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, `"finish_reason":"stop"`) {
|
||||
t.Errorf("plain answer finish_reason changed: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestToolIDSanitize locks the tool-call id sanitizer in both adapters that
|
||||
// talk to Claude upstreams. Anthropic requires tool_use.id /
|
||||
// tool_result.tool_use_id to match ^[a-zA-Z0-9_-]{1,64}$ and rejects the WHOLE
|
||||
|
||||
Reference in New Issue
Block a user