Ollama litellm continue cli
- 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 등의 파라미터가 실제로 드롭되고, 시스템 메시지에 주입되는지 로그로 검증한다.
- 동일한 요청을 실행한 뒤, LiteLLM 프록시 콘솔/JSON 로그에서
- outbound payload에 tools가 제거되고 system 프롬프트에 함수 정의/JSON 지시가 합성됐는지 확인
- (프록시 가동 시 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 주입(에뮬레이션) 경로, 강제 호출, 후속 도구 결과 주입, 스트리밍, 파라미터 드롭·주입 로그까지 일련의 동작을 단계적으로 검증할 수 있다.