Routes: support model IDs with slashes; add llama.cpp/Ollama compat probes

- /api/v1/models/{model_id} and /api/u/{username}/models/{model_id} used a
  single-segment param, so model IDs containing '/' (e.g. rotations/lisa)
  returned 404. Use {model_id:path}.
- Add capability-probe endpoints so OpenAI/Ollama/llama.cpp-style clients stop
  getting 404/405 on connect: GET /api/version, /v1/version, /version;
  GET /api/tags (Ollama model list); GET /props, /api/props, /v1/props.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 42e06a0c
...@@ -112,7 +112,7 @@ async def favicon(): ...@@ -112,7 +112,7 @@ async def favicon():
async def health(): async def health():
return {"status": "ok"} return {"status": "ok"}
@router.get("/api/v1/models/{model_id}") @router.get("/api/v1/models/{model_id:path}")
async def v1_get_model(model_id: str, request: Request): async def v1_get_model(model_id: str, request: Request):
all_models_response = await v1_list_all_models(request) all_models_response = await v1_list_all_models(request)
all_models = all_models_response.get("data", []) all_models = all_models_response.get("data", [])
...@@ -260,6 +260,72 @@ async def v1_chat_models_alias(request: Request): ...@@ -260,6 +260,72 @@ async def v1_chat_models_alias(request: Request):
async def models_root_alias(request: Request): async def models_root_alias(request: Request):
return await v1_list_all_models(request) return await v1_list_all_models(request)
# ---------------------------------------------------------------------------
# llama.cpp / Ollama compatibility probes
#
# Several OpenAI-compatible clients (LM Studio, Ollama-style frontends,
# llama.cpp UIs) probe capability endpoints on connect. Without these they get
# 404/405 (the bare /api/version & /api/props otherwise match the generic
# POST /api/{provider_id} route and return 405). We answer them with sensible
# stubs so capability detection succeeds.
# ---------------------------------------------------------------------------
def _aisbf_version() -> str:
try:
import aisbf
return getattr(aisbf, "__version__", "0.0.0")
except Exception:
return "0.0.0"
@router.get("/api/version")
@router.get("/v1/version")
@router.get("/version")
async def compat_version():
"""Ollama-style version probe."""
return {"version": _aisbf_version()}
@router.get("/api/tags")
async def compat_ollama_tags(request: Request):
"""Ollama-style model list (/api/tags). Maps the OpenAI model list into the
Ollama tags schema so Ollama-compatible clients can enumerate models."""
model_list = await _build_model_list(request)
data = model_list.get("data", []) if isinstance(model_list, dict) else []
models = []
for m in data:
model_id = m.get("id", "")
models.append({
"name": model_id,
"model": model_id,
"modified_at": "1970-01-01T00:00:00Z",
"size": 0,
"digest": "",
"details": {
"parent_model": "",
"format": "gguf",
"family": m.get("owned_by", "aisbf"),
"families": [m.get("owned_by", "aisbf")],
"parameter_size": "",
"quantization_level": "",
},
})
return {"models": models}
@router.get("/props")
@router.get("/api/props")
@router.get("/v1/props")
async def compat_llamacpp_props():
"""llama.cpp-style /props probe with minimal server properties."""
return {
"system_prompt": "",
"default_generation_settings": {
"n_ctx": 0,
"model": "aisbf",
},
"total_slots": 1,
"chat_template": "",
"model_path": "aisbf",
}
@router.post("/api/v1/audio/transcriptions") @router.post("/api/v1/audio/transcriptions")
async def v1_audio_transcriptions(request: Request): async def v1_audio_transcriptions(request: Request):
logger.info("=== V1 AUDIO TRANSCRIPTION REQUEST ===") logger.info("=== V1 AUDIO TRANSCRIPTION REQUEST ===")
......
...@@ -172,7 +172,7 @@ async def user_list_models(request: Request, username: str): ...@@ -172,7 +172,7 @@ async def user_list_models(request: Request, username: str):
logger.warning(f"Error listing user autoselect {autoselect_id}: {e}") logger.warning(f"Error listing user autoselect {autoselect_id}: {e}")
return {"object": "list", "data": all_models} return {"object": "list", "data": all_models}
@router.get("/api/u/{username}/models/{model_id}") @router.get("/api/u/{username}/models/{model_id:path}")
async def user_get_model(model_id: str, request: Request, username: str): async def user_get_model(model_id: str, request: Request, username: str):
result = await user_list_models(request, username) result = await user_list_models(request, username)
for model in (result.get("data", []) if isinstance(result, dict) else []): for model in (result.get("data", []) if isinstance(result, dict) else []):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment