Ollama litellm continue cli

From 흡혈양파의 인터넷工房
Jump to navigation Jump to search
ollama 와 litellm, 그리고 continue cli 에 대한 삽질 기록(20251020)

서문

20251020 글 작성 시점에서 cli 계열의 coding agent 는 tool call 이라는걸 지원해야 한다. 이는 기존의 단순한 대화형이 아니라 좀 더 구체적인 작업을 하기 위한 정해진 규걱? 같은 내용으로 보면 된다.

그런데 공개된 Model 들은 현재 시점에서 claude code 가 요구하는 수준의 tool call 을 처리하지 못한다. 적어도 ollama 는 그렇다(lm studio 는 좀 나은편)

그러다보니 공개된 model 에 tool 관련된 학습을 lora 등으로 진행해서 튜닝한 모델을이 몇가지 존재하는데 그다지 좋다고 볼 수는 없다. 그래서 이를 별도의 방법을 통해 처리하려는 시도를 몇가지 해 보았다.


각 요소들의 설졍

litellm : config.yaml

model_list:
  - model_name: qwen3-coder-30b-a3b  # 사용자가 호출할 모델 이름
    model_info:
        supports_function_calling: true
    litellm_params:
      model: ollama/danielsheep/Qwen3-Coder-30B-A3B-Instruct-1M-Unsloth:UD-Q5_K_XL # 실제 라우팅될될될 모델 (ollama/모
델명 형식)
      api_base: http://host.docker.internal:11434 # Ollama 컨테이너 주소
      max_tokens: 8192
      response_format:
        type: json_object
      # 백엔드(ollama)가 네이티브 tool/function calling을 미지원할 때
      # 혼선을 유발하는 파라미터를 확실히 제거
      additional_drop_params: []
        #  - tools
        #- tool_choice
        #- functions
        #- function_call

router_settings:
  remove_model_name_prefix: true
  num_retries: 3
  timeout: 4096
  retry_policy:
    retry_on_status_codes: [400,401,402,403,408,429,500,502,503]

litellm_settings:
  drop_params: false
  set_verbose: true
  force_tool_call_emulation: true

위의 설정을 통해서 litellm 에서 tool 관련된 중간처리를 하도록 설정한다. tool 을 지원하지 않는 model 의 응답을 JSON 형식으로 처리해서 tool 의 형식을 갖추도록 한다.


continue cli : config.yaml

name: Local via Ollama
version: 0.0.1
schema: v1

models:
  - name: qwen3-coder-30b-a3b
    provider: ollama           # 반드시 custom 사용
    model: danielsheep/Qwen3-Coder-30B-A3B-Instruct-1M-Unsloth:UD-Q5_K_XL
    apiBase: http://192.168.1.239:11434
    apiKey: user
    override:
      defaultCompletionOptions:
        # 모델이 처리할 수 있는 최대 입력 토큰 수를 8192로 설정
        contextLength: 8192

이건 최종 설정이다. litellm 을 중간에 사용하고 provider 를 "openai" 로 설정했는데 어차피 그렇게 해도 claude cli 에서 알아먹지 못하는 tool name 이 온다면 claude cli 는 그걸 화면상에 응답받은 JSON 을 바로 출력해버리게 된다. 그럴바에는 provider 를 "ollama" 로 지정하고, 서버의 답변을 continue cli 가 처리하도록 하는것이 낫다


테스트에 사용된 스크립트

litellm alive check

curl -s http://192.168.1.239:3333/health/readiness
curl -s http://192.168.1.239:3333/health/liveliness
curl -s -H "Authorization: Bearer sk-TEST" http://192.168.1.239:3333/v1/models
curl -s http://192.168.1.239:3333/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk-TEST" \
  -d '{
    "model": "qwen3-coder-30b-a3b",
    "response_format": {"type": "json_object"},
    "messages": [
      {"role":"user","content":"Return a JSON with {\"city\":\"Seoul\",\"unit\":\"C\"} keys filled appropriately."}
    ],
    "max_tokens": 50
  }'

테스트 1: 기본 tool 호출 트리거(auto)

  • 목적: tools 배열과 tool_choice=auto를 전달해 모델이 함수 호출 의도를 JSON으로 내보내는지 확인한다.
  • 기대: 응답의 choices.message.tool_calls에 get_weather가 생성되거나, JSON 형태로 도구 호출 의도가 나타난다(response_format로 구조화 유도).
curl -sS http://192.168.1.239:3333/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer sk-TEST' \
  -d '{
    "model": "qwen3-coder-30b-a3b",
    "response_format": { "type": "json_object" },
    "messages": [
      { "role": "system", "content": "You can call tools. If needed, return a JSON object describing the tool and args." },
      { "role": "user", "content": "서울 날씨 알려줘" }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_weather",
          "description": "Return weather by city",
          "parameters": {
            "type": "object",
            "properties": { "city": { "type": "string" } },
            "required": ["city"]
          }
        }
      }
    ],
    "tool_choice": "auto",
    "temperature": 0
  }'


테스트 2: 특정 도구 강제 호출

  • 목적: tool_choice로 특정 함수명을 지정해 강제 호출 경로를 검증한다.
  • 기대: tool_calls에 name=get_weather가 생성되며 arguments에 도시명이 포함된다.
curl -sS http://192.168.1.239:3333/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer sk-TEST' \
  -d '{
    "model": "qwen3-coder-30b-a3b",
    "response_format": { "type": "json_object" },
    "messages": [
      { "role": "user", "content": "부산 날씨 알려줘" }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_weather",
          "description": "Return weather by city",
          "parameters": {
            "type": "object",
            "properties": { "city": { "type": "string" } },
            "required": ["city"]
          }
        }
      }
    ],
    "tool_choice": { "type": "function", "function": { "name": "get_weather" } },
    "temperature": 0
  }'

return value

{"id":"chatcmpl-f8986d1d-71c4-472c-b9e3-e3351a3dccd9","created":1760942417,"model":"ollama/danielsheep/Qwen3-Coder-30B-A3B-Instruct-1M-Unsloth:UD-Q5_K_XL","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"{\"error\": {\"type\": \"llm_call_failed\", \"message\": \"{\\\"message\\\":\\\"Operation not allowed\\\"}\\n\"}}","role":"assistant"}}],"usage":{"completion_tokens":34,"prompt_tokens":19,"total_tokens":53}}

테스트 3: 도구 실행 결과 주입(후속 턴)

  • 목적: 1~2번에서 받은 tool_calls를 그대로 대화 이력에 포함하고, role=tool 메시지로 결과를 전달해 최종 답변 생성까지 확인한다.
  • 방법: 아래 예시에서 "CALL_ID_FROM_PREV" 위치에 이전 응답의 tool_calls.id를 넣는다.
  • 기대: 모델이 tool 결과를 반영한 자연어 답변을 생성한다(프록시가 OpenAI 호환 대화 포맷을 수용).
curl -sS http://192.168.1.239:3333/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer sk-TEST' \
  -d '{
    "model": "qwen3-coder-30b-a3b",
    "messages": [
      { "role": "system", "content": "You can call tools." },
      { "role": "user", "content": "서울 날씨 알려줘" },
      { "role": "assistant", "content": "", "tool_calls": [
          {
            "id": "chatcmpl-b7eacfae-4c87-42e7-96f2-dfd55502d68c",
            "type": "function",
            "function": { "name": "get_weather", "arguments": "{\"city\":\"서울\"}" }
          }
        ]
      },
      { "role": "tool", "tool_call_id": "chatcmpl-b7eacfae-4c87-42e7-96f2-dfd55502d68c", "content": "{\"temp_c\":18, \"condition\":\"cloudy\"}" }
    ],
    "temperature": 0
  }'


테스트 4: 스트리밍으로 확인

  • 목적: SSE 스트림으로 tool_calls 또는 구조화 출력을 단계적으로 확인한다.
  • 기대: event: chunk 스트림으로 delta.tool_calls 또는 JSON 파편이 전달되며 최종적으로 finish_reason이 표시된다.
curl -N http://192.168.1.239:3333/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer sk-TEST' \
  -d '{
    "model": "qwen3-coder-30b-a3b",
    "stream": true,
    "response_format": { "type": "json_object" },
    "messages": [
      { "role": "user", "content": "대전 날씨 알려줘" }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_weather",
          "description": "Return weather by city",
          "parameters": {
            "type": "object",
            "properties": { "city": { "type": "string" } },
            "required": ["city"]
          }
        }
      }
    ],
    "tool_choice": "auto",
    "temperature": 0
  }'


테스트 5: drop_params 동작 확인(로그 기반)

  • 목적: 도구 미지원 백엔드에서 tools 등의 파라미터가 실제로 드롭되고, 시스템 메시지에 주입되는지 로그로 검증한다.
  1. 동일한 요청을 실행한 뒤, LiteLLM 프록시 콘솔/JSON 로그에서
  2. outbound payload에 tools가 제거되고 system 프롬프트에 함수 정의/JSON 지시가 합성됐는지 확인
  3. (프록시 가동 시 set_verbose / JSON logs 활성화 필요)

기대: inbound에는 tools가 존재하지만, outbound(ollama로 나가는 페이로드)에는 tools가 사라지고 JSON 지시가 합성된 흔적이 보인다.

결론

그럭저럭은 동작하지만 C 로 작업된 프로그램을 개선하는 수즌으로는 분명히 한계가 있는 것으로 보인다. 현시점의 결과이므로 향후 새롭고 좋은 모델들이 나온다면 결과는 바뀔 수 있으니 참고해야 한다.


참고 메모

  • OpenAI 호환 입력 포맷(roles, tools, tool_choice, response_format)은 LiteLLM 프록시가 그대로 수용하며, 백엔드 미지원 항목은 drop_params/변환 레이어로 흡수된다.
  • JSON 강제는 response_format {type: json_object} 사용을 권장하며, 구조화 출력 파싱 안정성 향상에 유용하다.
  • 프록시 엔드포인트/경로와 기동 방식은 LiteLLM “AI Gateway(Proxy)” 가이드의 기본값을 따른다.

위 시나리오로 tool 주입(에뮬레이션) 경로, 강제 호출, 후속 도구 결과 주입, 스트리밍, 파라미터 드롭·주입 로그까지 일련의 동작을 단계적으로 검증할 수 있다.


참고자료

https://docs.continue.dev/reference