본문으로 건너뛰기

릴레이 서버 요청/응답 스펙

이 페이지는 Editor AI SDK의 tool loop를 실행하는 호스트 애플리케이션과 릴레이 서버 사이의 스펙을 정의합니다. 직접 createMessage()를 구현한다면 요청 부분을, 직접 릴레이 서버를 구현한다면 응답과 오류 부분을 참고하십시오. Playground와 Thinkfree Relay Server가 이 스펙을 따르며, 릴레이 서버 가이드에서 서버 측을 구현한 Node.js 스켈레톤을 제공합니다.

엔드포인트

메서드와 경로목적성공 응답
POST /ai-agent/relay모델 요청 하나를 공급자에게 전달공급자 응답 그대로. JSON, 또는 streamtrue이면 SSE
OPTIONS /ai-agent/relayCORS preflight204와 허용 헤더
GET /healthliveness 확인{"ok":true,"relayPath":"/ai-agent/relay"}
POST /ai-agent/relay/files/upload첨부파일을 공급자의 파일 저장소에 업로드1{"uri":"…","name":"…"}
POST /ai-agent/relay/files/delete공급자 파일 저장소에서 파일 삭제공급자 응답, 또는 {"success":true}

1. 파일 저장소를 제공하는 공급자에서만 동작합니다. 지원 범위는 파일 helper에서 설명합니다.

릴레이 경로는 배포 환경에 따라 정할 수 있습니다. /ai-agent/relay는 Thinkfree Relay Server가 사용하는 경로이며, Playground는 이 값을 하드코딩하지 않고 설정에서 읽습니다. 릴레이 네임스페이스 안의 다른 경로는 route-not-found(404)와 함께 오류 본문을 반환하고, 릴레이 경로에 POST 외의 메서드로 요청하면 relay-method-unsupported(405)를 반환합니다.

요청 본문

POST /ai-agent/relay, Content-Type: application/json입니다. 본문은 예약된 키 두 개를 가진 하나의 JSON 객체입니다. 그 밖의 최상위 키, 예를 들어 릴레이 서버가 확인하는 테넌트 식별자는 자유롭게 추가할 수 있으며, 예약된 두 이름과 겹치지 않아야 합니다.

{
"tfAgentRelay": { "…": "what this turn means" },
"tfAgentTransport": { "…": "which HTTP request to execute" }
}

스트리밍하지 않는 Claude 요청의 전체 예시입니다. Playground가 도구 하나를 사용하는 loop에서 보내는 형태입니다.

{
"tfAgentRelay": {
"requestId": "8c1d0f0e-2f7a-4d63-9b1e-3a5f6c7d8e90",
"turnId": "8c1d0f0e-2f7a-4d63-9b1e-3a5f6c7d8e90",
"runtimeId": "runtime-1",
"agentId": "office-agent",
"sessionId": "b1a2c3d4-0000-4000-8000-000000000001",
"aiProviderConfig": { "connectionMode": "relay", "aiProvider": "claude", "model": "claude-sonnet-5", "maxToolIterations": 8 },
"systemPrompt": "You edit the open Word document with the provided tools.",
"transcript": [
{ "role": "user", "content": "Append a short greeting.", "messageContent": { "parts": [{ "type": "text", "text": "Append a short greeting." }] } }
],
"input": { "parts": [{ "type": "text", "text": "Append a short greeting." }] },
"tools": [
{ "name": "insert_text", "description": "Append text to the document.", "inputSchema": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] } }
],
"context": {
"runtime": null,
"agent": null,
"session": { "sessionId": "b1a2c3d4-0000-4000-8000-000000000001", "sessionName": null, "sessionDisplayName": null, "properties": {}, "aiProviderConfig": { "connectionMode": "relay", "aiProvider": "claude", "model": "claude-sonnet-5" } },
"turn": { "requestId": "8c1d0f0e-2f7a-4d63-9b1e-3a5f6c7d8e90", "turnId": "8c1d0f0e-2f7a-4d63-9b1e-3a5f6c7d8e90", "turnName": null, "turnDisplayName": null, "properties": { "documentId": "doc-42" } }
}
},
"tfAgentTransport": {
"adapterId": "claude-messages",
"aiProvider": "claude",
"model": "claude-sonnet-5",
"stream": false,
"body": {
"max_tokens": 8192,
"system": "You edit the open Word document with the provided tools.",
"messages": [{ "role": "user", "content": "Append a short greeting." }],
"tools": [{ "name": "insert_text", "description": "Append text to the document.", "input_schema": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] } }]
}
}
}

tfAgentRelay: 이 turn의 의미

tfAgentRelay는 호스트 애플리케이션이 공급자 중립적인 형태로 만든 turn의 스냅샷입니다. 릴레이 서버는 이 정보를 로그 기록과, 공급자별 페이로드 형식에 의존하지 않아야 하는 정책 적용에 사용하며, 그중 일부만 tfAgentTransport와 대조합니다. 검사 열은 릴레이 서버가 확인하는 항목을 나타내며, 나머지 항목은 릴레이 서버가 자체 용도로 사용하도록 전달됩니다.

필드타입검사의미
requestIdstring오류 본문과 로그에 포함모델 요청 하나를 처음부터 끝까지 식별
turnIdstring오류 본문과 로그에 포함사용자 turn을 식별. 한 tool loop에 속한 여러 요청에서 같은 값
runtimeIdstring 또는 null없음애플리케이션에 runtime 인스턴스가 있을 때 그 식별자
agentIdstring없음대화를 소유하는 agent 설정의 식별자
sessionIdstring없음대화의 식별자. 문서 하나, 탭 하나, 작업 하나에 대응
aiProviderConfigobjectaiProvidermodeltfAgentTransport의 값과 일치해야 함호스트 애플리케이션이 사용 중인 것으로 인식하는 공급자 설정
relayVendorstring 또는 null둘 다 있으면 tfAgentTransport.vendor와 일치해야 함릴레이 서버가 여러 vendor를 담당할 때 애플리케이션이 선택한 vendor. vendor는 릴레이 서버에 설정된 공급자 경로의 이름
systemPromptstring없음이 turn에 적용되는 시스템 프롬프트
transcripttranscript 메시지 배열없음이 turn 이전의 대화 기록
input메시지 콘텐츠없음현재 사용자 입력
tools도구 기술자 배열없음모델에게 제공한 도구 카탈로그
contextobject형태만 검사. 각 범위는 null 또는 객체, properties도 객체runtime, agent, session, turn의 스냅샷

aiProviderConfig에는 connectionMode("relay"), aiProvider, model이 포함되며, 선택적으로 openAiCompatible(preset, OpenAI 호환 경로에서 사용), maxToolIterations, auth가 포함됩니다. relay 모드에서 auth는 존재하지 않거나 null입니다. 호스트 애플리케이션에는 전송할 공급자 인증 정보가 없기 때문입니다.

context에는 runtime, agent, session, turn 네 범위가 있습니다. 각 범위는 null이거나, 범위의 식별자와 선택적인 표시 이름, 그리고 애플리케이션이 자체 값(예: 문서나 테넌트 식별자)을 채우는 properties 객체를 가진 객체입니다. 이 위치는 릴레이 서버에 권한 확인 데이터를 전달하기에 적합합니다. 릴레이 서버는 properties의 형태만 검사하고, 공급자에게는 전달하지 않기 때문입니다.

tfAgentRelay 안의 세 가지 콘텐츠 형태는 다음과 같습니다.

형태필드
Transcript 메시지role("user", "assistant", "tool"), content(string), 선택적으로 messageContent, requestId, turnId, meta, timestamp
메시지 콘텐츠parts: { "type": "text", "text" }, { "type": "image", "source", "mediaType", "fileName" }, { "type": "file", … }의 배열
도구 기술자name, description, inputSchema(JSON Schema), 선택적으로 readOnlyHint, destructiveHint 같은 annotations

tfAgentTransport: 실행할 요청

tfAgentTransport는 릴레이 서버가 실행하는 요청입니다. body는 호스트 애플리케이션이 direct 모드에서 공급자 API를 호출할 때 전송하는 요청 본문과 같으므로 릴레이 서버는 어떤 변환도 수행하지 않습니다. 인증 정보와 최종 모델을 덧붙여 전달할 뿐입니다.

필드타입필수의미
adapterIdstringbody를 생성한 공급자 어댑터. 아래 표 참고
aiProviderstring공급자 계열: claude, google, openai, openai-compatible
modelstring항상 전송요청한 모델. 릴레이 서버는 운영자가 고정한 모델로 대체할 수 있으며, 일부 배포 환경은 모델이 없는 요청을 거부함
vendorstring릴레이 서버가 하나의 aiProvider에 여러 vendor를 두었을 때만릴레이 서버에 설정된 공급자 경로(vendor)의 이름. 같은 aiProvider에 인증 정보나 허용 모델이 다른 경로를 여러 개 둘 때 구분함. null 대신 키 자체를 생략
streambooleantrue이면 공급자의 SSE 스트림, false이면 JSON 본문 하나
pathHintstring아니오설정된 endpoint 뒤에 붙는 경로. OpenAI 호환 경로에서만 사용
query문자열 object아니오공급자 URL에 추가하는 query 매개변수. OpenAI 호환 경로에서만 사용
safeHeaders문자열 object아니오호스트 애플리케이션이 전달해도 안전하다고 판단한 헤더. 인증 정보 등 릴레이 서버 소유 헤더가 항상 우선
bodyobject공급자에게 바로 전달할 수 있는 요청 본문

adapterId는 호스트 애플리케이션이 어떤 공급자 어댑터로 body를 만들었는지 나타내며, aiProvider와 함께 릴레이 서버가 호출할 공급자 endpoint를 결정합니다. 현재 사용하는 조합은 다음과 같습니다.

adapterIdaiProvider릴레이 서버가 호출하는 공급자 endpoint
claude-messagesclaudeMessages API
google-gemini-3, google-gemini-2googlegenerateContent, streamtrue이면 streamGenerateContent?alt=sse
openai-chat-completionsopenaiChat Completions
openai-compatible-chat-completionsopenai-compatible설정된 endpoint에 pathHint를 붙인 경로. 기본값 chat/completions

공급자별 body를 tool loop에 필요한 필드만 남겨 보면 다음과 같습니다.

{ "max_tokens": 8192, "system": "…", "messages": [{ "role": "user", "content": "…" }], "tools": [{ "name": "insert_text", "description": "…", "input_schema": { "type": "object" } }] }
{ "systemInstruction": { "parts": [{ "text": "…" }] }, "contents": [{ "role": "user", "parts": [{ "text": "…" }] }], "tools": [{ "functionDeclarations": [{ "name": "insert_text", "description": "…", "parameters": { "type": "object" } }] }] }
{ "messages": [{ "role": "system", "content": "…" }, { "role": "user", "content": "…" }], "tools": [{ "type": "function", "function": { "name": "insert_text", "description": "…", "parameters": { "type": "object" } } }], "stream": false }

릴레이 서버는 Claude, OpenAI, OpenAI 호환 경로에서는 model을 본문에 추가하고, Gemini에서는 URL에 포함합니다. 인증 정보는 x-api-key(Claude), Authorization: Bearer(OpenAI와 OpenAI 호환의 기본값), 또는 key query 매개변수(Gemini)로 추가합니다. 대화 상태는 추가하지 않습니다. 본문에 이미 대화 전체가 포함되어 있기 때문입니다.

검증과 정책

릴레이 서버는 아래 순서로 요청을 검사하고 첫 번째 실패 지점에서 중단합니다. 모든 검사를 통과하기 전에는 공급자에게 어떤 요청도 보내지 않습니다.

순서검사오류 코드상태
1요청 Origin이 허용된 값인가cors-origin-not-allowed403
2본문이 존재하고, 크기 제한 이내이며, 유효한 JSON인가relay-request-empty
relay-request-too-large
relay-request-json-invalid
400
413
400
3본문, tfAgentRelay, tfAgentTransport, tfAgentTransport.body가 JSON 객체이고 context 범위의 형태가 올바른가relay-request-invalid400
4adapterId, aiProvider, stream이 올바른 타입으로 존재하고, modelvendor가 있다면 문자열인가relay-transport-adapter-missing
relay-transport-provider-missing
relay-transport-model-missing
relay-transport-vendor-invalid
relay-transport-stream-invalid
400
5tfAgentRelay.aiProviderConfigtfAgentTransport가 같은 공급자·모델·vendor를 가리키는가relay-provider-mismatch
relay-model-mismatch
relay-vendor-mismatch
400
6vendor 경로가 존재하고 aiProvider와 일치하는가relay-vendor-not-allowed
relay-provider-not-allowed
relay-vendor-required
relay-vendor-provider-mismatch
relay-provider-unsupported
403
403
400
400
400
7모델이 경로의 허용 목록에 있는가relay-model-not-allowed403
8경로에 인증 정보와 사용 가능한 설정이 있는가provider-config-missing
provider-config-invalid
relay-config-unavailable
500
500
503

정책 결정은 6번과 7번 검사에서 이루어집니다. 경로는 tfAgentTransport.vendor에 지정된 vendor, 없으면 tfAgentRelay.relayVendor, 둘 다 없으면 aiProvider가 일치하는 유일한 설정 경로입니다. 모델은 경로가 허용하는 경우 요청한 모델이고, 요청에 모델이 없으면 경로의 기본 모델입니다. 배포 환경에 따라 서버에서 모델을 고정할 수도 있습니다. 이 경우에도 요청은 모델을 명시해야 하며, 응답은 고정된 모델에서 생성됩니다.

응답

성공

릴레이 서버는 200으로 응답하고 공급자 응답을 변경하지 않고 전달하므로, 호스트 애플리케이션이 받는 것은 공급자 고유의 형식입니다. JSON이면 Content-Type도 공급자의 값입니다. 스트림이면 text/event-stream; charset=utf-8Cache-Control: no-cache, no-transform이며, 각 SSE 이벤트는 공급자가 전송한 그대로 도착합니다.

공급자tool loop가 읽는 위치
Claudetypetext 또는 tool_usecontent[] 블록. 모델이 도구 실행을 요청하는 동안 stop_reasontool_use
Geminitext 또는 functionCall을 가진 candidates[0].content.parts[], 그리고 candidates[0].finishReason
OpenAI와 OpenAI 호환choices[0].message.contentchoices[0].message.tool_calls[], 그리고 choices[0].finish_reason

어떤 변환도 일어나지 않으므로 direct 모드에서 relay 모드로 전환한 호스트 애플리케이션은 응답 처리 코드를 그대로 유지합니다. 요청 URL과 요청 본문의 구조만 바뀝니다.

오류 본문

릴레이 서버가 요청을 거부하거나 처리에 실패하면 릴레이 서버 고유의 JSON 본문으로 응답합니다. 공급자의 오류 본문은 전달하지 않습니다. 릴레이 서버가 내용을 요약하고, 공급자의 상태 코드를 relay.upstreamStatus에 기록합니다.

{
"error": {
"code": "relay-model-not-allowed",
"message": "Relay model \"claude-opus-5\" is not allowed.",
"details": { "model": "claude-opus-5" }
},
"relay": {
"requestId": "8c1d0f0e-2f7a-4d63-9b1e-3a5f6c7d8e90",
"turnId": "8c1d0f0e-2f7a-4d63-9b1e-3a5f6c7d8e90",
"upstreamStatus": null
}
}
필드의미
error.code이 페이지의 표에 정의된 안정적인 기계 판독용 코드
error.message사람이 읽는 설명. 최종 사용자가 아닌 개발자에게 표시하는 값
error.detailsnull, 또는 문제가 된 값을 담은 객체. 예: { "model" }, { "path" }, { "method" }, { "upstreamStatus" }
relay.requestId, relay.turnId본문을 읽을 수 있었다면 tfAgentRelay에서 복사한 값, 그렇지 않으면 null
relay.upstreamStatus실패 원인이 공급자에 있다면 공급자의 HTTP 상태 코드, 그렇지 않으면 null

검증을 통과한 뒤 호스트 애플리케이션이 받을 수 있는 코드는 다음과 같습니다.

코드상태시점
upstream-request-failed502공급자가 실패 상태로 응답. relay.upstreamStatus를 확인합니다. 401은 키 문제, 429는 공급자의 호출 빈도 제한
relay-config-unavailable503릴레이 서버는 실행 중이지만 사용 가능한 공급자 설정이 없음
relay-internal-error500릴레이 서버 내부의 예기치 않은 실패
route-not-found404릴레이 네임스페이스 안의 알 수 없는 경로. details.path에 요청 경로 포함
relay-method-unsupported405릴레이 경로에 POST 외의 메서드로 요청. details.method에 해당 메서드 포함

첫 번째 조각 이후에 실패한 스트리밍 응답은 상태 줄이 이미 전송되었으므로 이 오류 본문으로 대체할 수 없습니다. 릴레이 서버는 스트림을 종료하며, 호스트 애플리케이션은 불완전한 스트림을 실패한 요청으로 처리하고 재시도 전에 문서 상태를 확인해야 합니다.

CORS

릴레이 서버는 정확히 하나의 origin 또는 *를 허용합니다. 특정 origin을 설정하면 Origin 헤더가 다른 요청은 preflight를 포함해 라우팅 전에 cors-origin-not-allowed로 거부되며, 응답에 Access-Control-Allow-Credentials: true가 포함됩니다. *를 설정하면 모든 origin이 허용되고 credentials 헤더는 전송되지 않습니다. 서버 간 호출이나 curl처럼 Origin 헤더가 없는 요청은 CORS 대상이 아니므로 정상 처리됩니다. preflight 응답은 204이며 Access-Control-Allow-Methods: POST, GET, OPTIONS와, 요청된 헤더를 그대로 반환하는 Access-Control-Allow-Headers를 포함합니다.

파일 helper

파일 저장소를 제공하는 공급자의 첨부파일은 공급자 키가 서버에 남도록 릴레이 서버를 통해 업로드합니다. 두 helper 모두 선택적인 tfAgentRelay 객체를 받아 그 식별자를 오류 응답과 로그에 포함하며, 메인 엔드포인트와 같은 오류 본문으로 응답합니다.

엔드포인트요청 필드성공 응답
POST /ai-agent/relay/files/uploadmimeType, base64BodyGemini Files API의 { "uri", "name" }
POST /ai-agent/relay/files/deleteprovider("google" 또는 "openai"), 그리고 Google은 fileUri, OpenAI는 fileId공급자의 삭제 응답, 또는 { "success": true }

capability 조회

릴레이 서버는 허용하는 vendor와 모델을 공개하여 설정 UI가 그 선택지만 제시하도록 할 수 있습니다. 참고 구현은 GET /ai-agent/relay/capabilities에서 이 정보를 제공하지만, Thinkfree Relay Server는 제공하지 않으며 route-not-found로 응답합니다. 선택 기능으로 취급하고, 요청 자체가 이 기능에 의존하지 않도록 구현하십시오.

{
"protocolVersion": "1",
"relayPath": "/ai-agent/relay",
"capabilitiesPath": "/ai-agent/relay/capabilities",
"defaultVendor": "google",
"vendors": [
{ "vendor": "google", "displayName": "Google Gemini", "aiProvider": "google", "models": ["gemini-3.8-flash"], "defaultModel": "gemini-3.8-flash" }
]
}

응답에는 vendor, 표시 이름, 각 vendor가 대응하는 aiProvider, 허용 모델, 기본 모델이 포함됩니다. 키, 공급자 endpoint, 헤더·query 재정의 값은 포함되지 않습니다.