Skip to main content

제공자 감지

FIM One은 LiteLLM을 범용 어댑터로 사용합니다. core/model/openai_compatible.py의 _resolve_litellm_model() 함수는 사용자의 LLM_BASE_URL + LLM_MODEL을 제공자 접두사가 있는 LiteLLM 모델 식별자로 매핑합니다. 접두사는 LiteLLM이 요청을 라우팅하는 방식을 결정합니다 — 네이티브 API 프로토콜(Anthropic Messages API, Gemini 등) 또는 일반 OpenAI 호환 /v1/chat/completions. 해석 순서:
  1. 명시적 제공자 (DB ModelConfig.provider 필드에서) — 최우선 순위. 제공자가 URL의 알려진 도메인과 일치하면 api_base가 반환되지 않습니다(LiteLLM이 네이티브로 라우팅). 그렇지 않으면 api_base가 릴레이 URL로 설정됩니다.
  2. 도메인 매칭 KNOWN_DOMAINS 대비 — 공식 API 엔드포인트는 호스트명으로 인식됩니다.
  3. URL 경로 힌트 PATH_PROVIDER_HINTS 대비 — UniAPI와 같은 릴레이 플랫폼에서 경로의 /claude 또는 /anthropic이 업스트림 프로토콜을 나타냅니다.
  4. 폴백 — openai/ 접두사(일반 OpenAI 호환).
제공자 접두사가 네이티브 프로토콜(anthropic, gemini 등)이고 URL이 공식 엔드포인트가 아닐 때, LiteLLM은 네이티브 프로토콜을 사용하지만 릴레이의 api_base로 요청을 전송합니다. 이는 제공자별 동작 — 아래에 설명된 Bedrock prefill 문제 포함 — 이 요청이 공식 API로 가든 릴레이를 통해 가든 적용됨을 의미합니다.
릴레이 URL에 경로에 /claude가 포함되어 있으면 FIM One은 자동으로 Anthropic의 네이티브 프로토콜을 통해 라우팅합니다. 이는 보통 올바릅니다(더 나은 스트리밍, thinking 지원), 하지만 제공자별 동작이 적용됨을 의미합니다 — 아래에 설명된 Bedrock prefill 문제 포함.

tool_choice — 네 가지 모드

tool_choice 매개변수는 OpenAI 형식을 통해 표준화됩니다. LiteLLM은 요청을 보내기 전에 각 제공자의 네이티브 프로토콜로 변환합니다. "auto"와 강제({"type":"function",...}) 간의 구분은 FIM One의 모든 호환성 문제의 핵심입니다. 이 두 모드는 서로 다른 요구사항을 가진 완전히 다른 하위 시스템에서 사용됩니다.

tool_choice가 사용되는 곳

두 개의 서브시스템이 tool_choice를 사용하며, 이들은 근본적으로 다른 방식으로 사용합니다.

ReAct 엔진 — tool_choice=“auto”

ReAct 루프는 모델이 각 반복마다 결정해야 합니다: 도구를 호출할지, 아니면 최종 답변을 제공할지. 여기서는 "auto"만 의미가 있습니다 — 모델이 tool_calls를 생성하거나 텍스트 콘텐츠를 생성하는 것 중 자유롭게 선택합니다. 이는 모든 제공자, 모든 모델, 확장 사고를 포함한 모든 모드와 호환됩니다. ReAct 엔진은 abilities["tool_call"] = True일 때 네이티브 함수 호출(_run_native)을 사용하고, 그렇지 않으면 JSON-in-content 모드(_run_json)로 폴백합니다. 두 모드 모두 "auto"를 사용합니다 — 차이점은 도구가 tools 매개변수를 통해 전달되는지, 아니면 시스템 프롬프트에서 설명되는지입니다. 자세한 내용은 ReAct 엔진 — 이중 모드 실행을 참조하세요.

structured_llm_call — tool_choice=forced

한 번의 호출로 수행하는 구조화된 추출(스키마 주석, DAG 계획, 계획 분석)입니다. 모델이 특정 가상 함수를 호출하도록 강제하여 구조화된 JSON 출력을 보장합니다. 이 호출 지점에서 제공업체별 오류가 발생합니다. structured_llm_call은 3단계 성능 저하 체인을 구현합니다. 세 단계의 이름은 코드에서 따온 것이며, 번호는 아래 섹션에서 설명하는 네 가지 보장 계층의 번호와 일치하지 않습니다. 핵심적인 설계 차이는 다음과 같습니다. structured_llm_call의 fallback은 런타임에 수행됩니다. 각 단계를 동적으로 시도하고 예외를 포착하여 다음 단계로 넘어갑니다. 반면 ReAct 엔진의 모드 선택은 빌드 타임에 수행됩니다. 시작 시 _native_mode_active를 한 번 확인하고 전체 루프에서 하나의 모드로 실행합니다. 따라서 structured_llm_call은 제공업체별 400 오류가 발생해도 투명하게 복구할 수 있지만, ReAct는 처음부터 모드가 올바르게 선택되어 있어야 합니다.

구조화된 출력: 네 가지 보장 수준

“구조화된 출력”은 서로 다른 네 가지 메커니즘을 포괄하는 표현입니다. 이 메커니즘은 모두 JSON을 생성하지만 동일한 수준의 보장을 제공하지는 않습니다. 차이를 만드는 것은 제약이 적용되는 위치입니다. 스키마에서 금지한 토큰을 디코더가 생성할 수 없도록 디코더 내부에 제약이 적용되거나, 단지 요청만 할 수 있는 프롬프트 내부에 제약이 적용됩니다. T1과 T2를 뒷받침하는 메커니즘은 제약 디코딩입니다. 제공업체는 스키마를 문법으로 컴파일하고, 각 디코딩 단계에서 해당 출력이 스키마에 따라 파싱되지 않도록 만드는 모든 토큰을 마스킹합니다. 필수 필드는 반드시 출력되기 전까지 객체를 닫게 만드는 토큰이 허용 집합에 포함되지 않으므로 건너뛸 수 없습니다. 이는 “모델에 정중하게 요청했고 모델이 따랐다”는 주장과는 다른 종류의 보장이며, 프롬프트 준수가 보장되지 않는 temperature 1이나 소형 모델에서도 T1/T2가 보장되는 이유입니다.

T1과 T2는 서로 대체할 수 없습니다

두 방식은 서로 다른 채널을 통해 동일한 보장을 제공하며, 채널은 세 가지 측면에서 중요합니다. 턴의 형태가 다릅니다. T1은 도구 호출 턴을 반환하며, 모델이 행동하기로 결정했음을 의미합니다. T2는 일반적인 assistant 메시지를 반환하며, 모델이 특정 형태로 답변했음을 의미합니다. 호출자가 에이전트 루프가 아닌 하나의 추출만 원할 때 T2는 모델이 “호출”할 가상 함수를 만들어 내지 않고 그 의도를 직접 전달합니다. T1을 강제하면 사고 과정과 충돌합니다. T1에서 스키마로 제한된 페이로드를 얻으려면 일반적으로 선택을 강제해야 하며, required 또는 이름이 지정된 함수 중 하나를 사용합니다. 여러 provider는 확장된 사고 과정이 활성화된 상태에서 강제된 도구 선택을 거부합니다. 아래 표 B에는 이를 지원하는 provider가 정리되어 있습니다. T2에는 이러한 충돌이 없습니다. T2는 도구 제약이 아니라 응답 제약이므로, 사고 과정이 활성화된 상태에서도 동작하는 유일한 스키마 제한 경로입니다. 추론 모델을 표준으로 사용하는 배포 환경에서는 이것이 스타일이 아닌 실용적인 측면에서 T2를 선택해야 하는 이유입니다. T1은 T2에는 없는 스키마 연결 작업을 한 라운드 추가로 요구합니다. 가상 함수에는 이름과 설명이 필요하며, 모델이 해당 함수를 호출하지 않고 거부할 수 있도록 허용할지 결정해야 합니다.

T2의 비용

제약 디코딩은 무료가 아니며, 이미 보유한 스키마가 일반적으로 해당 스키마가 허용하는 스키마와 일치하지 않습니다.
  • 스키마의 부분 집합. OpenAI의 strict mode에서는 모든 객체에 additionalProperties: false를 지정하고 모든 속성을 required에 나열해야 합니다. 선택 사항은 required에서 생략하는 대신 null과의 유니온으로 표현합니다. 루트는 객체여야 합니다. 전체 속성 수와 중첩 깊이에도 제한이 있습니다. 직접 작성한 스키마 대부분은 사용 가능해지기 전에 수정이 필요합니다.
  • 문법 컴파일. 새로운 스키마를 포함하는 첫 번째 요청은 provider 측에서 일회성 컴파일 지연을 발생시킵니다. 안정적인 스키마를 재사용하면 이 비용을 분산할 수 있지만, 요청마다 새로운 스키마를 생성하면 그렇지 않습니다.
  • 새로운 실패 지점. 응답하지 않는 모델은 스키마 형태의 객체 대신 refusal을 반환하므로, 호출자는 이를 처리하는 분기를 추가해야 합니다.

어떤 tier도 보장하지 않는 것

모든 tier는 형식을 제한합니다. 진실성을 보장하는 tier는 없습니다. T2 응답은 스키마에 완전히 부합하면서도 사실과 다를 수 있으며, enum으로 제한된 필드는 허용된 값 중 어느 것도 올바르지 않은 경우에도 그중 하나를 반환합니다. 디코더의 역할은 정답을 아는 것이 아니라 출력이 문법의 범위 안에 있도록 유지하는 것이기 때문입니다. 스키마 게이팅은 파싱 실패와 필드 형식 오류를 제거합니다. 하지만 값이 무엇을 의미하는지 확인해야 할 필요성까지 없애지는 않습니다.
두 가지 번호 체계가 있으며, 서로 일치하지 않습니다. 이 페이지에서는 위의 보장 tier에 T1–T4를 사용합니다. 또한 structured_llm_call의 성능 저하 체계에 있는 세 단계에는 Level 1/2/3을 사용하며, 각 단계의 이름은 코드(native_fc, json_mode, plain_text)에서 따왔습니다. FIM One의 Level 2는 json_object이며, 이는 T2가 아닌 T3입니다. 이 체계에 세 단계만 있는 이유는 tier가 세 개뿐이어서가 아니라 T2를 건너뛰기 때문입니다.

FIM One의 위치

현재 설계의 정직한 한계는 다음 다섯 가지입니다.
  1. 체인에는 스키마로 제한된 대체 단계가 없습니다. Level 1이 실패하면 다음 단계는 json_object이며, 이는 텍스트가 파싱된다는 것만 보장합니다. 필드를 계속 강제하는 중간 단계는 없습니다.
  2. Level 1의 보장은 이름이 암시하는 것보다 약합니다. strict가 없으면 네이티브 함수 호출은 2024년 이전의 비엄격 방식으로 동작합니다. 모델은 일반적으로 스키마를 따르지만, 필수 필드를 누락하거나, 존재하지 않는 키를 만들거나, 숫자로 선언된 위치에 문자열을 반환하는 것이 방지되지는 않습니다.
  3. anthropic/ 경로에서는 Level 2가 실제로 T3가 아닙니다. Anthropic Messages API에는 response_format이 없으므로 LiteLLM은 assistant 프리필을 삽입해 JSON 모드를 에뮬레이션합니다. 이는 프롬프트 계층의 장치이므로 실질적인 보장은 T4와 T3 사이에 있으며, T3 수준의 보장을 제공하지 않습니다. 아래에 설명된 Bedrock 프리필 문제는 이 에뮬레이션이 명시적으로 실패하는 경우입니다. 더 조용한 문제는 매개변수 이름이 암시하는 보장이 애초에 제공되지 않았다는 점입니다.
  4. 이후 결과를 스키마에 대해 검증하는 과정이 없습니다. jsonschema는 의존성에 포함되어 있지 않습니다. 수행되는 검사는 무엇이든 선택적 parse_fn 내부의 호출 지점별 검사이며, 엄격성은 호출 지점마다 다릅니다.
  5. 실패가 드러나지 않고 흡수됩니다. 거의 모든 호출자가 default_value를 전달하므로, 체인을 모두 소진해도 예외가 발생하는 대신 그럴듯해 보이는 객체가 반환됩니다. StructuredCallResult.level_used는 값을 생성한 단계를 기록하지만, 어떤 호출 지점에서도 이를 읽지 않습니다. 또한 default_value 경로에서는 실제로 아무것도 생성되지 않은 체인인데도 plain_text로 보고합니다.
현재 실제로 어느 단계에 있는지 확인하는 방법. structured_llm_call은 완료된 호출마다 INFO 수준으로 한 줄을 기록합니다. 이 로그는 데이터가 parse_fn을 통과한 후에만 생성되므로, 실제로 성공한 단계를 나타냅니다.
전체 실패는 level=none outcome=default_value로 WARNING 수준에 기록됩니다. 하루 동안의 트래픽에서 이 두 줄을 grep하는 것이 특정 배포 환경에서 누락된 T2 단계가 비용을 발생시키는지 확인할 수 있는 유일한 방법입니다. default_value 설계 때문에 증상이 오류가 아니라 평범하지 못한 응답으로 나타나기 때문입니다.

각 tier의 Provider 지원

이 표는 FIM One의 코드 경로가 아니라 upstream API를 설명합니다. 이 페이지의 다른 모든 표에는 동작을 구현하는 함수가 명시되어 있습니다. 이 표에는 해당 함수를 명시할 수 없습니다. FIM One은 어떤 provider에서도 T2 기능을 사용하지 않으며, 어느 provider에도 strict를 설정하지 않기 때문입니다. 이 표는 T2 구현을 결정할 때 실제로 사용 가능한 기능을 기준으로 검토할 수 있도록 제공됩니다. Vendor의 기능은 빠르게 변경되며, 지원이 한 제품군의 일부 모델에 먼저 적용되는 경우가 많습니다. 따라서 표의 항목을 사용하기 전에 vendor의 공식 문서를 다시 확인하세요.
단순히 표를 다시 설명하는 것이 아니라 표의 형태를 이해하는 데 도움이 되는 세 가지 패턴을 살펴보겠습니다. T3는 보편적이지만 T2는 그렇지 않습니다. FIM One이 지원하는 모든 provider는 json_object를 제공하지만, schema tier를 제공하는 provider는 대략 절반입니다. 따라서 이식 가능한 structured-output 경로는 T3 이하를 기반으로 구축해야 하며, 이것이 FIM One의 chain이 현재와 같은 형태를 갖는 이유입니다. T2를 추가하려면 전역 switch가 아니라 model별 capability flag를 추가해야 합니다. 항상 활성화된 thinking은 T1 경로를 닫는 경향이 있습니다. GLM, Kimi의 thinking model, deepseek-reasoner는 모두 강제된 tool choice를 제한하거나 거부하며, Anthropic은 thinking이 활성화된 동안 이를 거부합니다. MiniMax는 이에 대한 반례입니다. 경로가 닫혀 있는 경우 T2가 남은 유일한 schema-gated option이므로, OpenAI보다 Chinese-model deployment에서 누락된 rung이 더 중요합니다. Self-hosting은 일반적인 우선순위를 뒤집습니다. Constrained decoding은 serving stack의 속성이므로, instruction-following이 frontier model보다 훨씬 약한 local checkpoint에서도 사용할 수 있습니다. Schema gating이 가장 필요한 model일수록 이를 적용할 수 있는 가능성도 가장 큽니다.

Bedrock prefill 함정

response_format={"type":"json_object"}이 anthropic/ 접두사로 해석된 모델에 전달되면, LiteLLM은 JSON 모드를 시뮬레이션하기 위해 내부적으로 어시스턴트 프리필 메시지를 주입합니다. Anthropic Messages API는 기본 response_format 매개변수가 없으므로, LiteLLM은 어시스턴트 콘텐츠로 여는 중괄호를 앞에 붙여서 근사합니다:
이는 Anthropic의 직접 API에서 작동합니다. 그러나 최신 AWS Bedrock 모델 버전은 마지막 메시지가 role: "assistant"인 대화를 거부합니다 — 이를 “어시스턴트 메시지 프리필”이라고 부르며 다음을 발생시킵니다:
이 오류는 세 가지 조건이 모두 동시에 충족될 때만 발생합니다:
  1. 모델이 anthropic/ 접두사로 해석됩니다(도메인 일치 또는 URL 경로 힌트를 통해).
  2. response_format={"type":"json_object"}이 전달됩니다(structured_llm_call의 json_mode 코드 경로).
  3. 실제 백엔드는 AWS Bedrock입니다(프리필을 거부함).
OpenAI 호환 엔드포인트를 통한 Bedrock? Bedrock 릴레이가 OpenAI 호환 /v1/chat/completions 엔드포인트를 노출하고(AWS의 자체 OpenAI 호환 게이트웨이 또는 제3자 프록시), URL 경로에 /claude 또는 /anthropic이 포함되지 않으면, FIM One은 이를 openai/ 접두사로 해석합니다. LiteLLM은 백엔드를 표준 OpenAI 호환 서버로 취급하고, 프리필 주입 없이 response_format을 직접 전달하며, 서버가 JSON 제약을 기본적으로 처리합니다. 프리필 함정이 적용되지 않습니다 — json_mode_enabled=false를 설정할 필요가 없습니다.
이는 기본 도구 호출(tool_choice="auto"과 tools= 매개변수 포함)에 영향을 주지 않습니다. 프리필 주입은 response_format에만 발생합니다. ReAct 에이전트 실행은 완전히 영향을 받지 않습니다.
Level 1(native_fc)과 Level 2(json_mode)가 모두 Bedrock에서 실패하면, 시스템은 Level 3(plain_text)에서 복구됩니다. 아래에 설명된 json_mode_enabled 플래그는 낭비되는 Level 2 호출을 제거합니다.

해결 방법: json_mode_enabled

모델별 json_mode_enabled 플래그는 Level 2 (json_mode)를 시도할지 여부를 제어합니다.
  • DB로 구성된 모델: 관리자 → 모델 → 고급 설정에서 토글합니다. 이 플래그는 ModelProviderModel.json_mode_enabled에 저장되며 기본값은 TRUE입니다.
  • ENV로 구성된 모델: 환경에 LLM_JSON_MODE_ENABLED=false를 설정합니다.
  • 효과: 비활성화하면 abilities["json_mode"]가 False를 반환하므로 response_format이 전달되지 않고, 프리필도 수행되지 않아 Bedrock이 작동합니다. 성능 저하 체인은 native_fc → plain_text가 되며, 문제가 발생하는 json_mode 호출을 완전히 건너뜁니다.
  • 건너뛰기의 비용: 시스템 프롬프트가 JSON 형식으로 반환하도록 요청하고 최신 모델에서는 extract_json()이 자유 형식 콘텐츠를 안정적으로 파싱하므로, 실제로 모델은 여전히 유효한 JSON을 반환합니다. 손실되는 것은 출력이 아니라 보장입니다. 이제 체인은 T4에서 끝나며, 결과를 제약하는 것은 프롬프트뿐입니다. anthropic/ 라우트에서는 에뮬레이트된 JSON 모드가 디코더 수준의 보장을 제공한 적이 없으므로 이러한 손실이 보이는 것보다 작습니다.

Thinking models + forced tool_choice

여러 제공자는 확장 사고가 활성화된 상태에서 강제 tool_choice를 거부합니다. 특정 함수 호출을 고정하는 것이 모델의 먼저 추론할 자유를 모순된다는 이유로:
이것은 thinking models의 법칙이 아니라 제공자별 규칙입니다. Anthropic은 프로토콜 수준에서 이를 강제하고 Moonshot(Kimi)도 같은 방식으로 동작하지만, MiniMax는 모든 호출에서 사고하며 여전히 강제 tool choice를 허용합니다. Provider Capability Matrix의 표 B는 제공자별로 판정을 기록합니다. 한 행에서 다른 행으로 일반화하지 마세요. Anthropic 모델의 경우, structured_llm_call은 native-FC 수준에서 reasoning_effort=None을 전달하여 충돌을 자체적으로 해결하며, 이는 해당 호출에 대해 사고를 끕니다(structured.py::_call_llm). 구조화된 출력은 깊은 추론이 아니라 스키마 준수가 필요하므로, 여기서 사고를 비활성화하는 것은 올바르고 더 저렴합니다. API를 통해 사고를 전환할 수 없는 경우, native_fc는 모든 구조화된 호출에서 400으로 실패하며 체인이 json_mode로 떨어지기 전에 약 10초가 소요됩니다. Kimi가 일반적인 경우입니다. 사고가 켜져 있을 때는 auto만 지원되며, 강제 tool choice는 사고를 꺼야 하는데, Moonshot은 이를 모델 id를 통해서만 노출합니다(kimi-k2는 꺼져 있고, kimi-k2.5와 kimi-k2-thinking은 켜져 있음). FIM One은 이를 전환하는 매개변수가 없으므로, 해결책은 아래의 tool_choice_enabled 플래그입니다.

해결책: tool_choice_enabled

모델별 tool_choice_enabled 플래그는 Level 1 (native_fc)이 시도되는지 여부를 제어합니다:
  • DB 구성 모델: Admin → Models → Advanced → “Native Function Calling”에서 토글합니다. 플래그는 ModelProviderModel.tool_choice_enabled에 저장됩니다 (기본값 TRUE).
  • ENV 구성 모델: 환경에서 LLM_TOOL_CHOICE_ENABLED=false를 설정합니다.
  • 효과: 비활성화되면 abilities["tool_choice"]는 False를 반환 → 저하 체인이 Level 2 (json_mode) 또는 Level 3 (plain_text)에서 시작되며, native_fc를 완전히 건너뜁니다. 이는 호환되지 않는 모델에 대한 구조화된 호출당 약 10초의 페널티를 제거합니다.
  • ReAct 에이전트 영향 없음: tool_choice_enabled는 structured_llm_call에서만 강제 도구 선택을 제어합니다. ReAct 엔진은 tool_choice="auto" (모델이 자유롭게 결정)를 사용하며, 이 설정과 관계없이 모든 모델에서 작동합니다.
tool_choice_enabled와 tool_call은 별도의 능력 플래그입니다. tool_call (OpenAICompatibleLLM의 경우 항상 True)은 도구가 모델에 전달되는지 여부를 제어합니다 — 비활성화하면 ReAct 에이전트가 손상됩니다. tool_choice는 구조화된 출력 추출을 위해 강제 도구 선택이 시도되는지만 제어합니다.
tool_choice="auto"는 사고 모드의 영향을 받지 않습니다. ReAct 엔진은 "auto"만 사용하므로, 사고가 활성화된 상태에서 에이전트 실행이 작동합니다.
이 제약을 피하기 위해 abilities["tool_call"] = False를 설정하지 마세요. 이는 ReAct의 _run_native 모드 (이는 tool_choice="auto"를 사용하고 사고와 잘 작동함)를 비활성화하여, 덜 안정적인 _run_json 모드로 강제합니다.
제공자 마이그레이션 참고: 일부 타사 릴레이는 reasoning_effort (drop_params=True)와 같은 지원되지 않는 매개변수를 자동으로 삭제하므로, 구성된 경우에도 사고가 활성화되지 않습니다. 사고를 적절히 지원하는 제공자 (Bedrock, 직접 Anthropic API)로 마이그레이션할 때, native_fc의 reasoning_effort=None은 일관된 동작을 보장합니다. 사용자 조치가 필요하지 않습니다 — 구조화된 출력은 모든 제공자에서 동일하게 작동합니다.

Provider Capability Matrix

이 섹션은 각 제공자가 지원하는 기능과 FIM One이 이에 대해 수행하는 작업의 권위 있는 기록입니다. 모든 행은 동작을 구현하는 함수의 이름을 지정하므로 여기서 제시된 모든 주장은 코드에 대해 확인할 수 있습니다. 다른 페이지는 데이터를 반복하는 대신 여기에 링크합니다. 코드가 변경되면 이 섹션도 함께 변경됩니다. 한 행은 단일 모델이 아닌 제공자의 프로토콜을 설명합니다. 한 제품군 내의 모델이 다른 경우(DeepSeek chat 대 reasoner, Kimi thinking 켜짐 대 꺼짐), 셀에 그 내용이 표시됩니다.

표 A: 프로토콜 라우팅

설정된 base_url과 model이 LiteLLM 호출로 변환되는 방식과, 첫 번째 인터페이스 선택지를 사용할 수 없을 때 발생하는 상황입니다. GPT-5.x가 프로토콜을 선택하는 방식. FIM_GPT5_RESPONSES_MODE가 프로토콜을 선택합니다. 기본값인 native는 litellm.aresponses를 통해 /v1/responses를 직접 호출하고, bridge는 LiteLLM의 chat-completions 변환을 사용하며, off는 일반 chat completions를 강제합니다. 브리지가 중요한 한 가지 정보를 손실하기 때문에 네이티브 경로가 필요합니다. 브리지는 추론 항목을 버리므로 GPT-5.x 에이전트는 도구를 호출할 때마다 사고 과정을 다시 도출합니다. 프로토콜을 직접 호출하면 해당 항목을 재생할 수 있습니다. reasoning_effort=None을 명시적으로 전달하는 호출은 chat completions를 사용합니다. structured_llm_call과 완료 신호 프로브가 이에 해당합니다. 생각하지 않도록 지정한 호출에는 보존할 추론 상태가 없기 때문입니다. 단, 추론을 끌 수 없는 모델(gpt-6.1-*, gpt-6-astra)은 예외입니다. 이 모델은 reasoning_effort 값과 관계없이 chat completions에서 함수 도구를 거부하므로, 해당 호출은 /v1/responses를 사용하며 가장 낮은 추론 수준인 low로 실행됩니다. 같은 이유로 off를 설정하거나 엔드포인트에 /v1/responses가 없으면 이 모델에서는 도구 호출을 사용할 수 없습니다. 네이티브 요청에서 중요한 속성 두 가지는 놓치기 쉽습니다.
  • store=false는 업스트림에서 대화 상태를 저장하지 않도록 하며, include=["reasoning.encrypted_content"]는 암호화된 페이로드를 반환하도록 요청합니다. include가 없으면 추론 항목은 비어 있는 상태로 도착하고, 재생은 아무런 표시 없이 작동하지 않게 됩니다.
  • 재생할 추론 항목에서는 서버 측 id를 제거해야 합니다. store=false이면 업스트림에 아무것도 저장되지 않으므로, id를 그대로 돌려보내면 Item with id 'rs_...' not found. Items are not persisted when 'store' is set to false 오류가 발생합니다. encrypted_content 블롭에 상태가 자체적으로 담겨 있으므로 id를 제거해도 손실되는 것은 없습니다(sanitize_reasoning_item).
Bedrock. Bedrock에서 호스팅되는 Claude는 Bedrock에서 호스팅된다는 사실이 아니라, 어떤 경로로 확인되었는지에 따라 동작합니다. anthropic/ 경로로 라우팅되는 릴레이를 거치면 Anthropic 프로토콜 동작을 따릅니다. 여기에는 최신 Bedrock 버전에서 거부되는 LiteLLM의 json-mode assistant 사전 입력도 포함됩니다. OpenAI 호환 게이트웨이를 거치면 openai/로 확인되고, 사전 입력은 추가되지 않으며 json_mode_enabled를 계속 활성화할 수 있습니다.

Table B: 충돌 및 해결 방법

FIM One은 네 가지 tool_choice 상태 중 세 가지를 발생시킵니다: ReAct 루프(react.py::_run_native)의 auto, 구조화된 출력(structured.py::_call_llm)의 명명된 함수, 그리고 finish-signal 답변이 도구 페이로드를 재생할 때의 none. 어떤 호출 사이트도 required를 발생시키지 않습니다. 해당 열은 required와 명명된 함수 모두에 적용되는 비auto 도구 선택에 대한 제공자의 제약을 기록합니다.

Table C: Thinking protocol

LLM_REASONING_EFFORT는 low, medium, high를 허용하며, 다른 값은 미설정으로 읽힙니다 (deps.py::_reasoning_effort). FIM One이 와이어에 전송하는 것은 제공자별이며, 이 표는 그것을 기록합니다. replay 열은 reasoning_replay_policy의 반환값으로, 제공자별 목록이 아닌 4가지 상태의 작은 폐쇄 집합입니다. unsupported와 informational_only는 와이어에서 동일한 바이트를 생성합니다: 둘 다 나가는 히스토리에서 reasoning_content와 signature를 제거합니다. 의도가 다르므로, 명확하게 reasoning하지만 unsupported에 속하는 모델은 라이브 버그가 아닌 조각 표의 간격입니다.

Relay/proxy gotchas

타사 게이트웨이는 직접 제공자와 다른 방식으로 실패하며, 대부분의 실패는 조용합니다. 아래의 각 행은 증상을 메커니즘과 짝지으며, FIM One이 이미 이에 대해 수행하는 작업을 설명합니다.
지원 범위. FIM One은 이 페이지에 문서화된 동작을 1차 엔드포인트에 대해 보장합니다: OpenAI의 자체 API, Anthropic, Google, 그리고 자신의 모델을 직접 제공하는 모든 공급업체. 타사 relay는 최선의 노력 기반으로 지원되며, 요청에 relay가 수행하는 작업이 우리의 제어 범위를 벗어나고 자주 자신의 문서 범위도 벗어나기 때문에 해당 보장의 적용을 받지 않습니다. Relay는 매개변수를 삭제하거나, 히스토리를 다시 쓰거나, 캐시 중단점을 제거하거나, 부분적으로만 구현하는 프로토콜에 응답할 수 있으며, 대부분의 경우 오류 대신 200을 반환합니다.이것은 우리가 약속하는 것에 대한 진술이지, 실행되는 것에 대한 제한이 아닙니다. FIM One은 승인된 호스트의 허용 목록을 유지하지 않으며, 여기서 아무것도 도메인에 의해 제한되지 않습니다. 기능은 엔드포인트가 실제로 수행하는 작업으로 결정됩니다: 누락된 경로는 404로 응답하고 기억되며, 무시된 include는 빈 추론 항목을 생성하고 재생은 작동 불가능해지며, 거부된 요청은 해당 호출에 대해 폴백됩니다. 엔드포인트를 조사하는 것이 호스트명에서 기능을 추론하는 것보다 더 정확하며, Azure OpenAI, 엔터프라이즈 게이트웨이, 그리고 프로토콜을 올바르게 구현하는 자체 호스팅 프록시에 대해 계속 작동하는 유일한 접근 방식입니다.Relay가 폴백이 포착하지 못하는 방식으로 오작동하는 경우, FIM_GPT5_RESPONSES_MODE(bridge 또는 off) 또는 모델별 tool_choice_enabled 및 json_mode_enabled 토글로 프로토콜을 직접 고정하고, FIM One 버그로 제출하기 전에 1차 엔드포인트에 대해 재현하세요.

모델별 권장 구성

tool_choice_enabled과 json_mode_enabled는 관리자 → 모델 → 고급 설정에서 모델별로 토글할 수 있습니다. 기본값인 TRUE는 대부분의 제공자에게 올바르지만, 오류나 불필요한 지연이 발생할 때만 조정하세요. 조정이 필요한 제공자는 위의 표 B에 기록되어 있으며, 운영자가 작성하는 모델별 보기는 모델 관리에 있습니다.
변경 시기: 로그에서 structured_llm_call: native_fc call raised 경고 다음에 성공적인 json_mode 추출이 표시되면, 해당 모델은 native_fc의 이점을 얻지 못합니다. 해당 모델에 대해 “Native Function Calling”을 비활성화하여 낭비되는 API 호출(구조화된 출력 요청당 약 10초)을 제거하세요.
ENV 수준 재정의는 환경 변수를 통해 구성된 모든 모델에 적용됩니다(관리자 UI 제외):

추론 노력 및 사고 구성

FIM One은 확장된 사고 / 추론을 제어하기 위해 두 개의 환경 변수를 노출합니다: 사고가 활성화되면 자동으로 따르는 두 가지 동작이 있으며, 둘 다 사용자 구성이 필요하지 않습니다:
  1. 온도는 자동으로 처리됩니다. 사고가 활성화된 anthropic/ 경로에서 _build_request_kwargs는 temperature를 1.0으로 고정합니다. 이는 Bedrock이 요구하는 값입니다. 샘플링 매개변수를 완전히 거부하는 모델(Opus 4.7 및 4.8, Fable 5, Mythos 5)은 사고 여부와 관계없이 요청에서 temperature가 제거됩니다. 이를 위해 LLM_TEMPERATURE=1을 수동으로 설정하지 마세요.
  2. GPT-5.x는 가능한 경우 도구와 추론을 함께 유지합니다. FIM One은 GPT-5.x에 대해 먼저 Responses 브리지를 조사합니다. 왜냐하면 그것이 둘을 결합하는 유일한 표면이기 때문입니다. 사용 가능한 /v1/responses 경로가 없는 엔드포인트는 채팅 완성으로 폴백하고, 판정은 엔드포인트별로 캐시되며, 해당 경로에서 tools를 전달하는 요청은 명시적 reasoning_effort none을 전송합니다. 필드를 생략하는 것은 동등하지 않습니다. 서버 기본값이 none이 아니기 때문입니다.

구조화된 출력을 위한 방어적 파싱

native_fc가 올바르게 작동하더라도, 구조화된 출력 파이프라인에는 모든 제공자 또는 호환성 계층의 엣지 케이스를 처리하기 위한 방어적 파싱 계층이 포함되어 있습니다. DAG 플래너의 _dict_to_steps 파서는 세 가지 일반적인 엣지 케이스를 처리합니다:
  1. 배열 대신 단일 객체. 일부 모델은 배열 {"steps": [{"id": "1", "task": "..."}]} 대신 {"steps": {"id": "1", "task": "..."}} (단일 스텝 객체)를 반환합니다. 파서는 id 또는 task 키를 확인하여 이를 감지하고 객체를 리스트로 래핑합니다.
  2. 이중 인코딩된 JSON 문자열. 구조화된 출력이 json_mode (스키마 강제가 없음)로 폴백될 때, 일부 제공자는 steps 값을 네이티브 배열 대신 JSON 문자열로 반환합니다 — 예: {"steps": "[{\"id\": \"1\", ...}]"}. 이 문자열에는 표준 json.loads를 깨뜨리는 리터럴 줄바꿈 (모델의 포매팅에서 발생)이 포함될 수도 있습니다. 파서는 extract_json_value() (이는 _repair_json_strings를 포함)를 사용하여 다음을 처리합니다:
    • JSON 문자열 값 내의 리터럴 줄바꿈
    • 잘못된 이스케이프 시퀀스 (LaTeX 또는 코드 콘텐츠에서 일반적)
    • 호환성 계층의 기타 직렬화 특이성
  3. 누락된 steps 래퍼. 모델이 steps 래퍼 키 없이 최상위 객체로 단일 스텝을 반환할 수 있습니다. 파서는 루트 레벨에서 id와 task를 감지하고 그에 따라 래핑합니다.
정상 작동 중에는 native_fc가 올바르게 구조화된 도구 호출 인자를 반환하며 이러한 엣지 케이스는 발생하지 않습니다. 방어적 파서는 사용자 정의 BaseLLM 서브클래스, 비정상적인 제공자 동작 또는 구조화된 출력이 json_mode 또는 plain_text로 저하되는 폴백 시나리오에 대한 안전망으로 존재합니다.

프롬프트 캐싱 (크로스 제공자)

FIM One은 Anthropic의 명시적 프롬프트 캐싱을 cache_control 중단점을 통해 구현하며, 동시에 프롬프트 섹션 레지스트리를 통해 다른 모든 제공자의 자동 접두사 캐싱의 이점을 누립니다. 목표는 호출별 프롬프트 형태 차이 없이 모든 제공자에서 작동하는 단일 프롬프트 조립 경로입니다.

아키텍처

fim_one.core.prompt 모듈은 세 가지 기본 요소를 노출합니다:
  • PromptSection — 정적 content: str 또는 동적 content: Callable을 가진 명명된 조각
  • PromptRegistry — 메모이제이션된 저장소 (정적 섹션은 한 번만 렌더링되고, 동적 섹션은 호출마다 다시 렌더링됨)
  • DYNAMIC_BOUNDARY — 레지스트리가 마지막 정적 섹션과 첫 번째 동적 섹션 사이에 삽입하는 센티널 마커로, 호출자가 캐시 중단점에서 렌더링된 提示词을 분할할 수 있도록 함
ReAct(JSON 모드, 네이티브 함수 호출 모드, 합성)의 시스템 提示词는 다음과 같이 분할됩니다:
  • 정적 접두사 (~提示词의 95%) — 정체성, 핵심 지침, 도구 설명
  • 동적 접미사 — 현재 날짜/시간, 요청별 언어 지시문, 인계 컨텍스트

기능 감지

fim_one.core.prompt.caching.is_cache_capable(model_id)은 모델 ID에 claude, anthropic, bedrock/anthropic, vertex_ai/claude 중 하나라도 포함되어 있으면 True를 반환합니다. 이러한 제공자들은 첫 번째 (정적) 메시지에 cache_control: {"type": "ephemeral"}을 포함한 두 개의 role="system" 메시지를 받습니다. 다른 모든 제공자는 cache_control 필드가 없는 단일 연결된 시스템 메시지를 받습니다. 이는 Anthropic이 아닌 엔드포인트들이 해당 필드를 거부하거나 자동으로 삭제하기 때문이며, 일부 릴레이를 통해 전송하면 400 unknown parameter 오류가 발생하기 때문입니다.

제공업체 간 지원 범위

PromptRegistry는 자동 접두사 캐싱을 지원하는 모든 제공업체에서 “무료로” 이점을 제공합니다. 정적 부분이 호출 간 바이트 단위로 동일하게 유지되도록 하고(현재 날짜 및 시간은 접두사가 아닌 동적 접미사에 포함), 각 자동 캐싱 제공업체의 해시가 일치해 캐시를 적중하게 합니다. 따라서 Anthropic 전용 cache_control을 고려하기 전부터 PromptRegistry는 모델에 종속되지 않는 근본적인 이점을 제공합니다.

관찰성

모든 chat/* 응답의 done_payload에는 이제 다음이 포함됩니다:
TurnProfiler는 턴당 구조화된 로그 라인을 내보냅니다: turn_cache summary | model=claude-sonnet-4-6 | read_tokens=1067 | create_tokens=0 | saved_input_tokens=961 (~90%). 이는 또한 릴레이 정직성 프로브로 기능합니다 — API 릴레이를 통해 라우팅하는 경우, 실제 청구된 입력과 read_tokens을 비교하여 릴레이가 cache_control을 제거하는지 또는 0.10× 할인을 유지하는지 감지할 수 있습니다. LLM 레이어에서는 달러 추정치가 반환되지 않습니다 — 가격 책정 및 릴레이 마크업이 위에 적용되므로 LLM 레이어는 객관적인 token 개수만 반환합니다.

멀티턴 캐시 ROI

Claude 4 ReAct 턴에서 기본 에이전트 프롬프트로 측정: 10개 도구를 사용한 10회 반복 ReAct 실행은 첫 번째 이후 각 턴마다 ~8,640개의 입력 토큰을 절약합니다(9개 캐시 히트 × 1067 토큰 × 90%). Anthropic은 첫 번째 호출에서 캐시 쓰기에 1.25배를 청구하므로, 손익분기점은 두 번째 호출에서 달성됩니다 — 단일 쿼리는 이점을 얻지 못합니다.

추론 재생 정책 (모델 없는 정확성)

확장 사고 / 추론 블록은 제공자마다 다르게 작동합니다. 균일한 직렬화 정책은 프로토콜 계약과 자동 접두사 캐시를 모두 깨뜨립니다. fim_one.core.prompt.reasoning.reasoning_replay_policy(model_id)는 네 가지 값 중 하나를 반환하며 OpenAICompatibleLLM._build_request_kwargs()에서 ChatMessage.to_openai_dict(replay_policy=...)를 제어합니다.

네 가지 정책

  • anthropic_thinking — Claude 계열 (anthropic/, bedrock/anthropic, vertex_ai/claude). 사고 블록은 반드시 signature를 첨부하여 재생해야 합니다. Anthropic은 서명이 누락되거나 변경되면 이후 턴을 거부합니다.
  • informational_only — CoT를 생성하지만 재생을 기대하지 않는 모델: DeepSeek 추론 모드 (V3.2의 deepseek-reasoner, 그리고 프래그먼트 테이블이 여전히 일치하는 이전 deepseek-r1 및 R1-Distill ID), Qwen QwQ, Gemini flash-thinking, OpenAI o1 / o3 / o4. 이들의 문서는 명시적으로 “메시지 히스토리에서 reasoning_content를 다시 보내지 마세요”라고 말합니다. 어쨌든 전송하면:
    • 제공자 계약을 위반합니다 (향후 버전에서 거부하기 시작할 수 있음)
    • 자동 접두사 캐시를 조용히 무효화합니다 — 메시지 바이트가 매 턴마다 변경되어 해시를 깨뜨립니다
  • openai_responses — GPT-5.x, gpt-5 프래그먼트에서 일치합니다. 이 모델의 추론 상태는 텍스트가 아니라 암호화된 페이로드를 전달하는 불투명한 항목의 시퀀스이며, /v1/responses만 이들을 위한 슬롯을 가집니다. 해당 프로토콜에서 항목은 그대로 재생되며, 이것이 도구 라운드 전체에서 모델의 사고 연쇄를 유지하는 것입니다. 읽을 수 있는 요약은 여전히 나가는 요청에서 삭제되므로, chat-completions 폴백에서는 informational_only와 정확히 동일하게 작동합니다. 정보 프래그먼트 앞에서 확인되며, 이들의 일반 reasoning 항목은 그렇지 않으면 프록시 태그된 GPT-5 ID를 삼킬 것입니다.
  • unsupported — 포괄적인 경우: 추론 기능이 없는 모델 (GPT-4o, Gemini 1.5, Mistral, Llama), 그리고 ID가 프래그먼트와 일치하지 않는 추론 모델 (GLM, MiniMax, Kimi, Doubao). 어느 쪽이든 필드를 재생해서는 안 되므로, 이 정책은 informational_only와 동일한 바이트를 전선에 배치합니다. 또한 알 수 없는 모델 ID에 대한 안전한 기본값입니다.
읽을 수 있는 reasoning_content와 불투명한 reasoning_items는 ChatMessage의 독립적인 필드입니다. to_openai_dict()는 항목을 전혀 직렬화하지 않으므로, 정책이 무엇이든 chat-completions 요청에 누출되는 것이 구조적으로 불가능합니다.

강제 실행

모든 정책 평가는 한 곳(_build_request_kwargs)에서 발생합니다. ChatMessage.to_openai_dict(replay_policy=None)은 A3 허용적 기본값을 유지하므로 조정되지 않은 호출자들이 회귀하지 않습니다. 크로스 제공자 테스트 매트릭스는 tests/test_reasoning_replay_policy.py에 있으며 역방향 어설션이 비-Anthropic 요청이 reasoning_content를 유출하지 않음을 증명합니다.

사용자용

기능 및 버그 동작은 자동이므로 아무것도 구성할 필요가 없습니다. 워크플로우 영향:
  • 같은 대화에서 Claude와 DeepSeek 간에 에이전트를 전환하는 경우, 히스토리는 thinking 블록이 그대로 저장되며, 다음 턴에서 발신 메시지 형태는 현재 모델에 맞게 조정됩니다.
  • 프록시 / 커스텀 BaseLLM 서브클래스를 사용하는 경우, 모델 id가 인식 가능한지 확인하세요(조각 중 하나를 포함). 그렇지 않으면 기본 unsupported 정책이 적용되며, 이는 안전하지만 비정상적인 프록시 뒤의 Claude가 thinking 재생을 잃을 수 있습니다. 모델 id 조각을 _CACHE_CAPABLE_MODEL_FRAGMENTS(core/prompt/caching.py에 있음) 및/또는 reasoning 정책 조회에 추가하세요.

문제 해결

“This model does not support assistant message prefill” Bedrock + json_mode. 두 가지 해결 방법: (1) LLM_JSON_MODE_ENABLED=false를 설정하거나 관리자 모델 설정에서 JSON Mode를 비활성화하거나; (2) Bedrock 제공자가 OpenAI 호환 /v1/chat/completions 엔드포인트를 제공하는 경우, 해당 엔드포인트로 전환하세요 — FIM One은 이를 openai/로 해석하며 prefill 주입이 발생하지 않습니다. “Thinking may not be enabled when tool_choice forces tool use” / “tool_choice ‘specified’ is incompatible with thinking enabled” Anthropic 모델의 경우, structured_llm_call은 native_fc 호출에 대해 자동으로 thinking을 비활성화합니다. kimi-k2.5, kimi-k2-thinking 또는 deepseek-reasoner와 같이 API를 통해 thinking을 끌 수 없는 경우, 모델의 고급 설정에서 “Native Function Calling”을 비활성화하거나 전역적으로 LLM_TOOL_CHOICE_ENABLED=false를 설정하세요. 성능 저하 체인은 native_fc를 건너뛰고 대신 json_mode 또는 plain_text를 통해 구조화된 출력을 추출합니다. thinking 모델이 이 문제를 가지고 있다고 가정하기 전에 Provider Capability Matrix의 표 B를 확인하세요. MiniMax는 이 문제가 없습니다. “DAG pipeline failed: LLM ‘steps’ is not an array” LLM이 steps 필드를 문자열 또는 단일 객체로 반환했습니다. 이는 일반적으로 구조화된 출력이 json_mode로 폴백되었음을 의미합니다(스키마 강제가 없음). 로그에서 structured_llm_call: level=xxx를 확인하세요 — native_fc 대신 json_mode를 표시하면 native_fc가 자동으로 실패하고 있습니다. 사용자 정의 BaseLLM 서브클래스를 사용하는 경우, reasoning_effort kwarg를 수락하는지 확인하세요. ReAct가 예기치 않게 JSON mode로 폴백됨 모델의 abilities["tool_call"]이 True인지 확인하세요. 이는 OpenAICompatibleLLM의 경우 항상 True이지만, 사용자 정의 BaseLLM 서브클래스는 이를 재정의할 수 있습니다. 관리자 API의 모델 상세 엔드포인트로 확인하세요. structured_llm_call이 모든 레벨을 소진하고 StructuredOutputError를 발생시킴 모델이 어떤 레벨에서도 파싱 가능한 JSON을 생성하지 못했습니다. 이는 최신 모델에서는 드문 일입니다. 다음을 확인하세요: (1) 스키마가 유효한 JSON Schema인지, (2) 모델이 전체 응답을 생성할 수 있는 충분한 max_tokens를 가지고 있는지, (3) 시스템 프롬프트가 스키마 지침과 모순되지 않는지. DAG 플래너와 분석기 모두 default_value 폴백을 제공하므로, 이 오류는 명시적으로 기본값을 생략하는 호출 사이트에서만 전파됩니다.