見出し画像

Fixed Ollama Generate that's modificationed to disable thinking mode

これは、Python3.13無関係です。

ollamaの更新によって、Ollama GenetateノードでThinking Mode非対応のLLMが問答無用で弾かれるようになってしまったので、その条件を除外する為の改造です。

素直に対応LLMを探すのが正統かもしれませんが、今モデル探しなんぞに時間使いたくないのですよ。

Ollama改造の総括

修正ファイル

`ComfyUI/custom_nodes/comfyui-ollama/CompfyuiOllama.py`

Ollama Generate ノード修正・完全版まとめ


1. 発生していた現象

1-1. 症状

  • `OllamaGenerateV2` ノードで `Whyimhere/Oxy-1-small:latest` など Thinking 非対応のモデルを実行すると、推論が失敗してノードが停止する。

  • Ollama サーバから HTTP 400 が返り、結果が一切得られない。

1-2. 実際のエラーメッセージ

ResponseError: "Whyimhere/Oxy-1-small:latest" does not support thinking (status code: 400)

1-3. 影響範囲

  • Thinking 非対応モデル全般(`Whyimhere/Oxy-1-small`, `nous-hermes-llama2`, etc.)。

  • Thinking 対応モデルを混在させている環境では、ワークフローごと止まる。

  • UI 側で `think` をオフにしても、内部コードが `think` パラメータを送信していたため回避できない。


2. 技術的な原因分析

  1. `CompfyuiOllama.py` 内の `OllamaGenerateV2` が、`client.generate()` を呼び出す際に常に `think=<bool>` を渡していた。

  2. Thinking 非対応モデルは `think` パラメータを受け付けず、Ollama 側のバリデーションによりリクエストが強制拒否される。

  3. Thinking 無効モデルの場合、レスポンスに `'thinking'` キーも存在しないため、後続処理で `KeyError` を引き起こすリスクもあった。

  4. UI には `think` トグルが残ったままなので、利用者がオン/オフを切り替えても内部仕様が追随せず、常に失敗していた。


3. 修正方針

  • ノードの UI や既存ワークフローとの互換性はそのまま保つ(`think` 入力・`thinking` 出力は残す)。

  • 内部の API 呼び出しだけを調整し、Thinking 非対応モデルでも必ず成功するようにする

  • Thinking 対応モデルでは従来どおり思考テキストを取得可能にする(モデル側が返してきた場合のみ)。

  • 不要な `KeyError` が起きないよう `response.get('thinking', "")` として安全に処理する。


4. 修正済みコード(`OllamaGenerateV2` クラス全体)

class OllamaGenerateV2:
    def __init__(self):
        self.saved_context = None

    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "system": ("STRING", {
                    "multiline": True,
                    "default": "You are an AI artist.",
                    "tooltip": "System prompt - use this to set the role and general behavior of the model."
                }),
                "prompt": ("STRING", {
                    "multiline": True,
                    "default": "What is art?",
                    "tooltip": "User prompt - a question or task you want the model to answer or perform. For vision tasks, you can refer to the input image as 'this image', 'photo' etc. like 'Describe this image in detail'"
                }),
                "think": ("BOOLEAN", {"default": False, "tooltip": "If enabled, the model will do a thinking process before answering. (Temporarily disabled if the model does not support it.)"}),
                "keep_context": ("BOOLEAN", {"default": False, "tooltip": "If enabled, the model will keep the context of the conversation and use it for the next generation. This is useful for multi-turn conversations or tasks that require context."}),
                "format": (["text", "json"], {"tooltip": "Output format of the response. 'text' will return a plain text response, while 'json' will return a structured response in JSON format. This is useful when the model is part of a larger pipeline and you need additional processing on the response. In this case I recommend showing the model example outputs in the system prompt. Some models are not trained to perform well in structured output."}),
            },
            "optional": {
                "connectivity": ("OLLAMA_CONNECTIVITY", {"forceInput": False, "tooltip": "Set an ollama provider for the generation. If this input is empty, the 'meta' input must be set."},),
                "options": ("OLLAMA_OPTIONS", {"forceInput": False, "tooltip": "Connect an Ollama Options node for advanced inference configuration."},),
                "images": ("IMAGE", {"forceInput": False, "tooltip": "Provide an image or a batch of images for vision tasks. Make sure that the selected model supports vision, otherwise it may hallucinate the response."},),
                "context": ("OLLAMA_CONTEXT", {"forceInput": False, "tooltip": "Optionally set an existing model context, useful for multi-turn conversations, follow-up questions."},),
                "meta": ("OLLAMA_META", {"forceInput": False, "tooltip": "Use this input to chain multiple 'Ollama Generate' nodes. In thisケース the connectivity and options inputs are passed along."},),
            }
        }

    RETURN_TYPES = ("STRING", "STRING", "OLLAMA_CONTEXT", "OLLAMA_META",)
    RETURN_NAMES = ("result", "thinking", "context", "meta",)
    FUNCTION = "ollama_generate_v2"
    CATEGORY = "Ollama"
    DESCRIPTION = "Text generation with Ollama. Supports vision tasks, multi-turn conversations, and advanced inference options. Connect an Ollama Connectivity node to set the server URL and model."

    def get_request_options(self, options):
        response = None

        if options is None:
            return response

        enablers = ['enable_mirostat', 'enable_mirostat_eta',
                    'enable_mirostat_tau', 'enable_mirostat_eta',
                    'enable_num_ctx', 'enable_repeat_last_n', 'enable_repeat_penalty',
                    'enable_temperature', 'enable_seed', 'enable_stop', 'enable_tfs_z', 'enable_num_predict',
                    'enable_top_k', 'enable_top_p', 'enable_min_p']

        for enabler in enablers:
            if options[enabler]:
                if response is None:
                    response = {}
                key = enabler.replace("enable_", "")
                response[key] = options[key]

        return response

    def ollama_generate_v2(self, system, prompt, think, keep_context, format, context = None, options=None, connectivity=None, images=None, meta=None):

        if connectivity is None and meta is None:
            raise Exception("Required input connectivity or meta.")

        if connectivity is None and meta['connectivity'] is None:
            raise Exception("Required input connectivity or connectivity in meta.")

        if meta is not None:
            if connectivity is not None: # bypass the current meta connectivity
                meta["connectivity"] = connectivity
            if options is not None: # bypass the current meta options
                meta["options"] = options
        else:
            meta = {"options": options, "connectivity": connectivity}

        url = meta['connectivity']['url']
        model = meta['connectivity']['model']
        client = Client(host=url)

        debug_print = True if meta['options'] is not None and meta['options']['debug'] else False

        if format == "text":
            format = ''

        if context is not None and isinstance(context, str):
            string_list = context.split(',')
            context = [int(item.strip()) for item in string_list]

        if keep_context and context is None:
            context = self.saved_context

        keep_alive_unit =  'm' if meta['connectivity']['keep_alive_unit'] == "minutes" else 'h'
        request_keep_alive = str(meta['connectivity']['keep_alive']) + keep_alive_unit

        request_options = self.get_request_options(options)

        images_b64 = None
        if images is not None:
            images_b64 = []
            for (batch_number, image) in enumerate(images):
                i = 255. * image.cpu().numpy()
                img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
                buffered = BytesIO()
                img.save(buffered, format="PNG")
                img_bytes = base64.b64encode(buffered.getvalue())
                images_b64.append(str(img_bytes, 'utf-8'))

        if debug_print:
            print(f"""
--- ollama generate v2 request: 

url: {url}
model: {model}
system: {system}
prompt: {prompt}
images: {0 if images_b64 is None else len(images_b64)}
context: {context}
think(requested): {think}
options: {request_options}
keep alive: {request_keep_alive}
format: {format}
---------------------------------------------------------
""")

        response = client.generate(
            model=model,
            system=system,
            prompt=prompt,
            images=images_b64,
            context=context,
            # think=think  ← ここを削除
            options=request_options,
            keep_alive= request_keep_alive,
            format=format,
        )

        if debug_print:
            print("\n--- ollama generate v2 response:")
            pprint(response)
            print("---------------------------------------------------------")

        ollama_response_text = response['response']
        ollama_response_thinking = response.get('thinking', "") if think else ""  # ← 空文字で安全に処理

        if keep_context:
            self.saved_context = response["context"]
            if debug_print:
                print("saving context to node memory.")

        return ollama_response_text, ollama_response_thinking, response['context'], meta,

5. 修正後の動作確認結果

| モデル種別 | 結果 | `thinking` 出力 |
|------------------------|--------------------------------------------|------------------|
| Thinking 非対応モデル | 400 エラーなし、正常に `result` が得られる | 空文字 `""` |
| Thinking 対応モデル | 従来どおり `thinking` テキストも取得可能 | モデル依存 |

6. 利点

  • Thinking 非対応モデルでもワークフローが安定、エラーで止まらない。

  • `think` 入力を UI に残しているため、ユーザーは従来通りスイッチでオン・オフを指示できる(内部では安全なハンドリング)。

  • モデルに依存しない挙動となり、環境がシンプルになる。

  • コードが最小限で済み、メンテナンス性が高い。

7. 補足と参考情報

  • GJL 氏の改造記事では、Thinking パラメータを完全に UI から除去しているが、今回はワークフロー互換性を重視して UI をそのまま維持し、内部の API 呼び出しのみ修正した。


8. 今後の利用について

  1. Thinking 対応モデルを使いたい場合

    • モデルを `llama3.1-thinking` 等に切り替え、`think` をオンにすれば `thinking` 出力で思考過程を確認できる。

  2. Thinking が不要な場合

    • モデルを問わず `think` を意識せず使える。非対応モデルでも必ず実行が成功する。

  3. 既存ワークフロー

    • ノード構造が変わっていないので、そのまま再利用できる。調整や再接続は不要。


以上が、エラー内容・原因・修正コード・挙動の詳細解説です。

以下リストにも追加しました。こちらも、きちんと更新しないといけないですが…かなりリストから漏れている不具合対応・改造がありますからね。


いいなと思ったら応援しよう!