mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-08-30 17:13:08 +08:00
cdc8ab78b5
Promote visual_provider_id/visual_model_key to dedicated ai_model columns, force non-stream upstream orchestration for visual paths, and synthesize SSE chunks for streaming Responses requests that include images.
213 lines
6.8 KiB
Go
213 lines
6.8 KiB
Go
// Copyright 2019 Yunion
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package codexconfig
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBuildOpenAIBaseURL(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"https://gw.example.com", "https://gw.example.com/ai/openai/v1"},
|
|
{"https://gw.example.com/", "https://gw.example.com/ai/openai/v1"},
|
|
{"https://gw.example.com/ai/openai/v1", "https://gw.example.com/ai/openai/v1"},
|
|
{"https://gw.example.com/openai/v1", "https://gw.example.com/ai/openai/v1"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := BuildOpenAIBaseURL(tc.in); got != tc.want {
|
|
t.Fatalf("BuildOpenAIBaseURL(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateConfigToml(t *testing.T) {
|
|
codexHome := t.TempDir()
|
|
catalog := BuildCatalogFromIDs([]ModelListEntry{{ID: "my-route/gpt-4", OwnedBy: "openai"}})
|
|
var out bytes.Buffer
|
|
if err := GenerateConfigToml(&out, "my-route/gpt-4", "https://gw/ai/openai/v1", "aiproxy", "", codexHome, catalog); err != nil {
|
|
t.Fatalf("GenerateConfigToml() error = %v", err)
|
|
}
|
|
generated := out.String()
|
|
for _, want := range []string{
|
|
`model = "my-route/gpt-4"`,
|
|
`base_url = "https://gw/ai/openai/v1"`,
|
|
`wire_api = "responses"`,
|
|
"[model_providers.aiproxy]",
|
|
`env_key = "OPENAI_API_KEY"`,
|
|
filepath.Join(codexHome, codexEnvFileName),
|
|
filepath.Join(codexHome, catalogFileName),
|
|
"[mcp_servers.deepwiki]",
|
|
} {
|
|
if !strings.Contains(generated, want) {
|
|
t.Fatalf("missing %q in generated config:\n%s", want, generated)
|
|
}
|
|
}
|
|
if strings.Contains(generated, "requires_openai_auth") {
|
|
t.Fatalf("should not require openai auth: %s", generated)
|
|
}
|
|
}
|
|
|
|
func TestGenerateConfigTomlStdoutMode(t *testing.T) {
|
|
catalog := BuildCatalogFromIDs([]ModelListEntry{{ID: "qwen-turbo", OwnedBy: "aliyun"}})
|
|
var out bytes.Buffer
|
|
if err := GenerateConfigToml(&out, "qwen-turbo", "https://gw/ai/openai/v1", "aiproxy", "", "", catalog); err != nil {
|
|
t.Fatalf("GenerateConfigToml() error = %v", err)
|
|
}
|
|
generated := out.String()
|
|
if strings.Contains(generated, "model_catalog_json") {
|
|
t.Fatalf("stdout mode should not include model_catalog_json: %s", generated)
|
|
}
|
|
}
|
|
|
|
func TestGenerateEnvFile(t *testing.T) {
|
|
out := GenerateEnvFile(`vk"secret`)
|
|
want := `export OPENAI_API_KEY="vk\"secret"`
|
|
if out != want+"\n" {
|
|
t.Fatalf("GenerateEnvFile = %q, want %q", out, want+"\n")
|
|
}
|
|
}
|
|
|
|
func TestIntersectModelEntries(t *testing.T) {
|
|
entries := []ModelListEntry{
|
|
{ID: "route-a/m1", OwnedBy: "aliyun"},
|
|
{ID: "route-b/m2", OwnedBy: "openai"},
|
|
{ID: "route-a/m3", OwnedBy: "aliyun"},
|
|
}
|
|
allowed := map[string]struct{}{
|
|
"route-a/m1": {},
|
|
"route-a/m3": {},
|
|
}
|
|
got := intersectModelEntries(entries, allowed)
|
|
if len(got) != 2 {
|
|
t.Fatalf("len(got) = %d, want 2", len(got))
|
|
}
|
|
if got[0].ID != "route-a/m1" || got[1].ID != "route-a/m3" {
|
|
t.Fatalf("got = %#v", got)
|
|
}
|
|
if len(intersectModelEntries(entries, map[string]struct{}{})) != 0 {
|
|
t.Fatal("expected empty intersection")
|
|
}
|
|
}
|
|
|
|
func TestWriteCodexFiles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
catalog := BuildCatalogFromIDs([]ModelListEntry{{ID: "gpt-4o", OwnedBy: "openai"}})
|
|
if err := writeCodexFiles(dir, "gpt-4o", "https://gw/ai/openai/v1", "aiproxy", defaultProviderDisplayName, GenerateEnvFile("vk-test"), catalog); err != nil {
|
|
t.Fatalf("writeCodexFiles() error = %v", err)
|
|
}
|
|
for _, name := range []string{"config.toml", catalogFileName, codexEnvFileName} {
|
|
if _, err := os.Stat(filepath.Join(dir, name)); err != nil {
|
|
t.Fatalf("missing %s: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDisplayNameFromSlug(t *testing.T) {
|
|
cases := map[string]string{
|
|
"gpt-5.5-codex": "GPT 5.5 Codex",
|
|
"deepseek-v4-pro": "Deepseek V4 Pro",
|
|
}
|
|
for slug, want := range cases {
|
|
if got := DisplayNameFromSlug(slug); got != want {
|
|
t.Fatalf("DisplayNameFromSlug(%q) = %q, want %q", slug, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildCatalogFromIDs(t *testing.T) {
|
|
models := BuildCatalogFromIDs([]ModelListEntry{
|
|
{ID: "route/qwen-turbo", OwnedBy: "aliyun"},
|
|
{ID: "gpt-4o", OwnedBy: "openai"},
|
|
})
|
|
if len(models) != 2 {
|
|
t.Fatalf("len(models) = %d, want 2", len(models))
|
|
}
|
|
if models[0].Slug != "gpt-4o" {
|
|
t.Fatalf("first slug = %q, want gpt-4o", models[0].Slug)
|
|
}
|
|
if models[0].ShellType != "unified_exec" {
|
|
t.Fatalf("shell_type = %q", models[0].ShellType)
|
|
}
|
|
if models[0].TruncationPolicy.Limit != defaultCatalogTruncationLimit {
|
|
t.Fatalf("truncation limit = %d", models[0].TruncationPolicy.Limit)
|
|
}
|
|
if models[0].BaseInstructions == "" {
|
|
t.Fatal("base_instructions should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestEnsureModelInCatalog(t *testing.T) {
|
|
catalog := EnsureModelInCatalog(nil, "custom-model", nil)
|
|
if len(catalog) != 1 || catalog[0].Slug != "custom-model" {
|
|
t.Fatalf("catalog = %+v", catalog)
|
|
}
|
|
if len(catalog[0].InputModalities) != 1 || catalog[0].InputModalities[0] != "text" {
|
|
t.Fatalf("default modalities = %#v", catalog[0].InputModalities)
|
|
}
|
|
catalog = EnsureModelInCatalog(catalog, "custom-model", nil)
|
|
if len(catalog) != 1 {
|
|
t.Fatalf("duplicate append: %+v", catalog)
|
|
}
|
|
visual := map[string]struct{}{"visual-model": {}}
|
|
catalog = EnsureModelInCatalog(nil, "visual-model", visual)
|
|
if len(catalog) != 1 || catalog[0].Slug != "visual-model" {
|
|
t.Fatalf("visual catalog = %+v", catalog)
|
|
}
|
|
if len(catalog[0].InputModalities) != 2 || catalog[0].InputModalities[1] != "image" {
|
|
t.Fatalf("visual modalities = %#v", catalog[0].InputModalities)
|
|
}
|
|
if !catalog[0].SupportsImageDetailOriginal {
|
|
t.Fatal("expected SupportsImageDetailOriginal")
|
|
}
|
|
}
|
|
|
|
func TestWriteModelsCatalogJSON(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), catalogFileName)
|
|
models := BuildCatalogFromIDs([]ModelListEntry{{ID: "gpt-4o", OwnedBy: "openai"}})
|
|
if err := WriteModelsCatalog(path, models); err != nil {
|
|
t.Fatalf("WriteModelsCatalog() error = %v", err)
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile() error = %v", err)
|
|
}
|
|
for _, field := range []string{
|
|
`"support_verbosity"`,
|
|
`"supports_search_tool"`,
|
|
`"default_verbosity"`,
|
|
} {
|
|
if !strings.Contains(string(raw), field) {
|
|
t.Fatalf("catalog JSON missing %s:\n%s", field, string(raw))
|
|
}
|
|
}
|
|
var parsed struct {
|
|
Models []ModelInfo `json:"models"`
|
|
}
|
|
if err := json.Unmarshal(raw, &parsed); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
if len(parsed.Models) != 1 || parsed.Models[0].Slug != "gpt-4o" {
|
|
t.Fatalf("parsed models = %+v", parsed.Models)
|
|
}
|
|
}
|