Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ GEMINI_MODELS={}

# --- Optional ---
# DEEPSEEK_PROFILE_DIR=session/profile # reuse an existing signed-in Chrome profile
# HOST=127.0.0.1
# HOST=0.0.0.0 # all interfaces; use 127.0.0.1 for loopback-only
# PORT=8000
# RATE_LIMIT_PER_MINUTE=30 # per-client-IP limit on /v1 endpoints

Expand Down
6 changes: 4 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Entry point — run the OpenAI-compatible DeepSeek/Qwen server.

python app.py # serves on http://localhost:8000
python app.py # serves on http://0.0.0.0:8000 (localhost + LAN IP)

On first use (no saved session) a browser window opens for you to sign in by
hand; run `python -m deepseek.auth` to do that ahead of time. Set
Expand All @@ -17,7 +17,9 @@
if __name__ == "__main__":
uvicorn.run(
"server.api:app",
host=os.getenv("HOST", "127.0.0.1"),
# 0.0.0.0: localhost and this machine's LAN IP. Set HOST=127.0.0.1 to
# keep the API loopback-only (recommended on untrusted networks).
host=os.getenv("HOST", "0.0.0.0"),
port=int(os.getenv("PORT", "8000")),
reload=False,
proxy_headers=False,
Expand Down
16 changes: 12 additions & 4 deletions integrations/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,26 +64,34 @@ def discover(base_url: str, without_browser=False) -> list:

def catalog_models(catalog: dict, without_browser=False) -> list:
rows = catalog.get("data") if isinstance(catalog, dict) else None
if not isinstance(rows, list) or not rows or len(rows) > 100:
# Bare ids plus owner/id aliases roughly double the catalog size.
if not isinstance(rows, list) or not rows or len(rows) > 200:
raise ValueError("Bridge advertises no models or an invalid catalog; enable a provider first")
models, seen = [], set()
for row in rows:
if not isinstance(row, dict):
raise ValueError("Invalid bridge model entry")
model = row.get("id")
owner = row.get("owned_by")
if (not isinstance(model, str) or not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}", model)
# Bare ids and matching `owner/id` aliases from the bridge catalog.
if (not isinstance(model, str)
or not re.fullmatch(
r"(?:[a-z][a-z0-9-]{0,31}/)?[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}", model)
or model in seen or not isinstance(owner, str) or owner not in PROVIDERS):
raise ValueError("Invalid or duplicate bridge model identity")
bare = model.split("/", 1)[-1]
if "/" in model and not model.startswith(owner + "/"):
raise ValueError("Invalid or duplicate bridge model identity")
seen.add(model)
models.append({"id": model, "name": LABELS.get(model, model + " · bridge"),
models.append({"id": model, "name": LABELS.get(bare, LABELS.get(model, model + " · bridge")),
"input": ["text"], "reasoningEfforts": False,
"contextWindow": 32768, "maxTokens": 4096})
# Validate the entire advertised catalog before excluding any provider.
if without_browser:
models = [model for row, model in zip(rows, models)
if row["owned_by"] in BROWSERLESS_PROVIDERS
or (row['owned_by'] == 'kimi' and row['id'] in KIMI_PUBLIC_MODELS)]
or (row["owned_by"] == "kimi"
and row["id"].split("/", 1)[-1] in KIMI_PUBLIC_MODELS)]
if not models:
raise ValueError("Bridge advertises no browserless models for --without-browser")
return models
Expand Down
5 changes: 3 additions & 2 deletions server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
SERVER_INTERACTIVE_LOGIN,
SESSION_REFRESH_ENABLED,
SESSION_REFRESH_INTERVAL,
advertised_model_ids,
is_known_model,
resolve_model_type,
model_provider,
Expand Down Expand Up @@ -604,7 +605,7 @@ def list_models():
"object": "list",
"data": [
{"id": name, "object": "model", "created": created, "owned_by": model_provider(name)}
for name in MODEL_MAP
for name in advertised_model_ids()
],
}

Expand All @@ -617,7 +618,7 @@ async def chat_completions(req: ChatCompletionRequest):
if not is_known_model(req.model):
return _error(
f"The model `{req.model}` does not exist. Available models: "
f"{', '.join(MODEL_MAP)}",
f"{', '.join(advertised_model_ids())}",
status=404, err_type="model_not_found",
)

Expand Down
38 changes: 35 additions & 3 deletions server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,52 @@
OPTIONAL_MODEL_PROVIDERS[name] = "gemini"


def model_provider(name: str) -> str:
def _bare_owner(name: str) -> str:
return OPTIONAL_MODEL_PROVIDERS.get(name, "qwen" if name in QWEN_MODEL_MAP else "deepseek")


def canonical_model(name: str):
"""Bare public id for a known model, or None.

Accepts both `qwen3.8-omni-flash` and `qwen/qwen3.8-omni-flash` when the
author segment matches the model's provider.
"""
if name in MODEL_MAP:
return name
if "/" not in name:
return None
owner, _, bare = name.partition("/")
if "/" in bare or bare not in MODEL_MAP or _bare_owner(bare) != owner:
return None
return bare


def model_provider(name: str) -> str:
bare = canonical_model(name)
return _bare_owner(bare if bare is not None else name)


DEFAULT_MODEL = "qwen3.8-omni-flash"


def is_known_model(name: str) -> bool:
"""Whether `name` is a model id we accept (used to 404 unknown models)."""
return name in MODEL_MAP
return canonical_model(name) is not None


def resolve_model_type(name: str) -> str:
"""Translate a public model id to the provider's wire model value.

Caller must check `is_known_model` first; this raises KeyError otherwise.
"""
return MODEL_MAP[name]
bare = canonical_model(name)
if bare is None:
raise KeyError(name)
return MODEL_MAP[bare]


def advertised_model_ids():
"""Bare ids plus matching `owner/id` aliases for OpenAI-compatible clients."""
for name in MODEL_MAP:
yield name
yield f"{_bare_owner(name)}/{name}"
17 changes: 17 additions & 0 deletions tests/test_harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,23 @@ def test_without_browser_keeps_only_the_explicit_direct_kimi_models(self):
self.assertEqual([m['id'] for m in models], ['kimi-instant', 'kimi-k3', 'kimi-k2.8'])
self.assertEqual(models[0]['name'], 'Kimi Instant · web')

def test_catalog_accepts_matching_owner_prefixed_aliases(self):
catalog = {"data": [
{"id": "qwen3.8-omni-flash", "owned_by": "qwen"},
{"id": "qwen/qwen3.8-omni-flash", "owned_by": "qwen"},
{"id": "kimi/kimi-instant", "owned_by": "kimi"},
]}
models = harness.catalog_models(catalog)
self.assertEqual([model["id"] for model in models],
["qwen3.8-omni-flash", "qwen/qwen3.8-omni-flash", "kimi/kimi-instant"])
self.assertEqual(models[1]["name"], "Qwen3.8 Omni Flash · web")
self.assertEqual(
[model["id"] for model in harness.catalog_models(catalog, without_browser=True)],
["qwen3.8-omni-flash", "qwen/qwen3.8-omni-flash", "kimi/kimi-instant"],
)
with self.assertRaises(ValueError):
harness.catalog_models({"data": [{"id": "evil/qwen3.8-omni-flash", "owned_by": "qwen"}]})

def run_cli_catalog(self, catalog, *args):
requests = []
real_client = httpx.Client
Expand Down
16 changes: 16 additions & 0 deletions tests/test_qwen_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,24 @@ async def test_models_have_correct_provider_ownership(self):
response = await self.http.get("/v1/models")
models = {m["id"]: m["owned_by"] for m in response.json()["data"]}
self.assertEqual(models["qwen3.8-omni-flash"], "qwen")
self.assertEqual(models["qwen/qwen3.8-omni-flash"], "qwen")
self.assertEqual(models["qwen3.8-max"], "qwen")
self.assertEqual(models["qwen/qwen3.8-max"], "qwen")
self.assertEqual(models["deepseek-chat"], "deepseek")
self.assertEqual(models["deepseek/deepseek-chat"], "deepseek")

async def test_author_prefixed_model_ids_are_accepted(self):
fake = Mock()
fake.chat.return_value = Reply("QWEN_OK", CID)
with patch.object(api, "get_qwen_client", return_value=fake), patch.object(api, "get_client") as ds:
response = await self.http.post(
"/v1/chat/completions",
json={**self.body, "model": "qwen/qwen3.8-max"},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["model"], "qwen/qwen3.8-max")
self.assertEqual(fake.chat.call_args.args[2], "qwen3.8-max")
ds.assert_not_called()

async def test_qwen_requests_do_not_use_deepseek_credentials(self):
fake = Mock()
Expand Down