File size: 4,867 Bytes
f0347b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
"""Provider-aware OpenAI-compatible client(s) for Daimon's models (F0/F2x).

Each modality (text, vision, omni, tts) is routed independently via
<MODALITY>_MODEL_PROVIDER in .env:

    local        -> llama.cpp / llama-server on this machine (text only)
    hf_inference -> Hugging Face Inference Providers / a ZeroGPU Space (any modality)

small-model-whisperer extends this with constrained decoding (GBNF / json-schema)
for the appraisal step in F2.

TEXT_THINKING_MODE=true switches MiniCPM5 into "thinking" mode (enable_thinking,
temp/top_p 0.9/0.95 per the model's deployment cookbook) for the main reply.
Callers that need deterministic, grammar-constrained output (e.g. F2's appraisal
step) pass enable_thinking=False explicitly to override this regardless of the
env switch - reasoning tokens would otherwise eat into a small max_tokens budget
before the grammar-constrained JSON.

Smoke test:
    python model/client.py
"""

import os
from pathlib import Path

from dotenv import load_dotenv
from openai import OpenAI

load_dotenv(Path(__file__).resolve().parent.parent / ".env")

MODALITIES = ("text", "vision", "omni", "tts")

THINKING_MODE = os.environ.get("TEXT_THINKING_MODE", "false").strip().lower() in ("1", "true", "yes")


def provider_for(modality: str) -> str:
    if modality not in MODALITIES:
        raise ValueError(f"unknown modality {modality!r}, expected one of {MODALITIES}")
    return os.environ.get(f"{modality.upper()}_MODEL_PROVIDER", "local")


def get_client(modality: str = "text") -> tuple[OpenAI, str]:
    """Return (OpenAI client, model name) for `modality`, based on its provider switch."""
    provider = provider_for(modality)

    if provider == "local":
        if modality != "text":
            raise ValueError("local provider only serves the text model (MiniCPM5-1B); "
                              f"set {modality.upper()}_MODEL_PROVIDER to hf_inference")
        base_url = os.environ.get("MODEL_BASE_URL", "http://localhost:8080/v1")
        return OpenAI(base_url=base_url, api_key="sk-no-key-needed"), "local-model"

    if provider == "hf_inference":
        token = os.environ["HF_TOKEN"]
        base_url = os.environ.get("HF_INFERENCE_BASE_URL") or "https://router.huggingface.co/v1"
        model = os.environ[f"HF_{modality.upper()}_MODEL"]
        return OpenAI(base_url=base_url, api_key=token), model

    raise ValueError(f"unknown provider {provider!r} for {modality.upper()}_MODEL_PROVIDER")


def _prepare_local_kwargs(kwargs: dict, enable_thinking: bool | None) -> dict:
    """Set chat_template_kwargs.enable_thinking and matching temp/top_p defaults
    for the local llama.cpp endpoint. `enable_thinking=None` falls back to
    TEXT_THINKING_MODE; explicit True/False (e.g. appraise.py) always wins."""
    use_thinking = THINKING_MODE if enable_thinking is None else enable_thinking
    extra_body = kwargs.pop("extra_body", {})
    extra_body.setdefault("chat_template_kwargs", {}).setdefault("enable_thinking", use_thinking)
    kwargs["extra_body"] = extra_body
    if use_thinking:
        kwargs.setdefault("temperature", 0.9)
    else:
        kwargs.setdefault("temperature", 0.7)
    kwargs.setdefault("top_p", 0.95)
    return kwargs


def chat(messages, modality: str = "text", *, enable_thinking: bool | None = None, **kwargs):
    """Send a chat completion for `modality` and return the text content."""
    client, model = get_client(modality)
    if provider_for(modality) == "local":
        kwargs = _prepare_local_kwargs(kwargs, enable_thinking)
    resp = client.chat.completions.create(model=model, messages=messages, **kwargs)
    return resp.choices[0].message.content


def chat_stream(messages, modality: str = "text", *, enable_thinking: bool | None = None, **kwargs):
    """Yield (kind, text) chunks as the reply streams in.

    `kind` is "thinking" for <think> reasoning tokens (only emitted when
    enable_thinking is on and the server reports `delta.reasoning_content`)
    and "content" for the actual reply text.
    """
    client, model = get_client(modality)
    if provider_for(modality) == "local":
        kwargs = _prepare_local_kwargs(kwargs, enable_thinking)
    stream = client.chat.completions.create(model=model, messages=messages, stream=True, **kwargs)
    for chunk in stream:
        delta = chunk.choices[0].delta
        reasoning = getattr(delta, "reasoning_content", None)
        if reasoning:
            yield "thinking", reasoning
        if delta.content:
            yield "content", delta.content


if __name__ == "__main__":
    client, model = get_client("text")
    print(f"Hitting {client.base_url} (model={model}, provider={provider_for('text')}) ...")
    out = chat(
        [{"role": "user", "content": "Reply with exactly: ok"}],
        max_tokens=8,
        temperature=0.0,
    )
    print("Model replied:", repr(out))